« Back to blog

CyLab Security Academy - RSA write-ups

Posted October 4, 2026· 15 min read · difficulty: beginner
Tags: python, crypto, write-up, ctf

Thinking through a little nibble of RSA.


“A man is crazy who writes a secret in any other way than one which will conceal it from the vulgar.” — Epistle on the Secret Works of Art and the Nullity of Magic, Roger Bacon

Fun With RSA

WOWOWOWOW what’s rsa you might ask? I’m glad you asked. here’s a little bit about our sponsor.

Rivest–Shamir–Adleman (RSA) Encryption

All positive integers can be written as the product of one or more prime factors.

25=5×525 = 5 \times 5 13=1313 = 13 1230487218961234=2×13×19×206477×120636431230487218961234 = 2 \times 13 \times 19 \times 206477 \times 12063643

Notice that for a large integer NN, it’s relatively difficult to decompose them into prime factors (i.e. no polynomial time algorithm currently exists, best one is sub-exponential)

Yet, it’s relatively easy to confirm that those prime factors are in fact the factors of that integer; just multiply any given prime factors and check if their product is equal to NN.

A process being easy one way and hard the other is an essential component of good cryptography. RSA encryption utilizes this property of prime factorization to encrypt and decrypt securely.

Setup

Step 1:

Choose distinct prime integers pp and qq that multiply to NN

p×q=np \times q = n

For example, let’s say p=67p = 67 and q=19q = 19 so N=1273N = 1273

Step 2:

Compute the Euler Totient function of N.

ϕ(N)=(p−1)×(q−1)\phi(N) = (p - 1) \times (q - 1)

For our numbers, ϕ(N)=(67−1)×(19−1)=1188\phi(N) = (67 - 1) \times (19 - 1) = 1188.

Step 3:

Choose ee between 1 and ϕ\phi where ee is coprime with ϕ\phi. ee is called the public exponent in (N,e) , since it’s used by anyone to encrypt a message to send to a person.

e∈(1,ϕ)∣gcd⁡(e,ϕ)=1e \in (1, \phi) | \gcd(e, \phi) = 1

Let’s say ee in our case is 5.

Step 4:

Calculate dd, the multiplicative inverse of emod  ϕe\mod\phi. Via the Extended Euclidean algorithm, our d=−475d = -475. It’s easier if we make it positive by adding ϕ\phi (we use ϕ\phi because adding ϕ\phi doesn’t change the value mod ϕ\phi as it’s divided out) so we don’t exponentiate by a negative number when decrypting. All in all, we get d=713d = 713. dd is called the private key in (N, d), since it’s only known by the receiver of an encrypted message and used to decrypt. This is also why RSA is asymmetric encryption since two different keys are used for encryption and decryption.

Encryption

Convert a message mm into a number. Bits represent characters and are numbers themselves, so we can use the bytes for each character as numbers to encrypt. If mm ends up being bigger than NN (which is not good since the message would get messed up through being taken mod NN), mm is divided into smaller portions. These are typically padded randomly to prevent RSA from becoming simple substitution and thus vulnerable to attacks on substitution ciphers.

Actually encrypting mm requires exponentiating it by ee, which is our case is 5, and then taking the result mod NN. Since we have such a small NN, we’ll need to split byte by byte. For the message “Hello, world!”, the bytes are

01001000 01100101 01101100 01101100 01101111 00101100 00100000 01110111 01101111 01110010 01101100 01100100 00100001

Converting each byte to decimal

72 101 108 108 111 44 32 119 111 114 108 100 33

and exponentiating each by ee produces

1934917632 10510100501 14693280768 14693280768 16850581551 164916224 33554432 23863536599 16850581551 19254145824 14693280768 10000000000 39135393

and finally mod NN gives

914 1183 337 337 213 347 698 807 213 456 337 693 827

Decryption

To decrypt, we exponentiate the ciphertext(s) by dd then take it mod NN, which gives us the original message. This is left as an exercise to the reader to verify, and another helpful exercise is just picking your own set up for RSA and attempting to use it to encrypt and decrypt!

The Challenges

Mind Your Ps and Qs

Decrypt my super sick RSA:
c: 717608817902276639011922897702552744324264244069272484013719744328086856277690292
n: 1368992139315019955067447961537607041364451719382349005056752379702694554965203011
e: 65537

Remember that NN is comprised of pp and qq, which is needed to calculate ϕ\phi and thus the private exponent dd required to decrypt. Looking at the problem statement, it appears that they’re saying NN is small, insinuating that NN can be easily factored. Put it into factordb.com and they’re right! Our factors are

p = 2074606527507983503944226222695401999839
q = 659880377875535146491994798968813949893149

Thus our ϕ\phi is

1368992139315019955067447961537607041363789764397945961926756440677503045613310024

Calculating dd such that ed≡1mod  ϕed\equiv 1\mod\phi, we then get

-33463927356800921128798261049227863348410687131933250393009503302948866732876431

where we can add dd to end up at

1335528211958219033938649700488379178015379077266012711533746937374554178880433593

We raise the ciphertext to the power of dd and get

print(pow(717608817902276639011922897702552744324264244069272484013719744328086856277690292, 1335528211958219033938649700488379178015379077266012711533746937374554178880433593, 1368992139315019955067447961537607041364451719382349005056752379702694554965203011))
# outputs 310924469227196415248432899983304451945245691282380935847926787838996952580711265

Convert to binary

101101000011011101100011000100100000110110100000000000100110111110010110110111010000110110111001000010100111110111100100110001010011110100110101010010010101001011101111010011000001001110000100001111011010011101111110000100110111111100110010000101100100100110110110111001

and the converting each byte to text, we get

§Ó#Sv66öFóuóåôåó×7·–ÖVF6

which… doesn’t look quite right.

Why?

Well, there’s 8 bits in a byte and so our binary should have a multiple of 8 for the number of bits. However, we only have 268 digits which is not a multiple of 8. We’re off by 4 digits to get a whole number of bytes. This is because when we converted to binary, the leading zeros didn’t come with; you wouldn’t write 5 as 05 normally, perhaps only as formatting for a date. Similarly, converting to binary doesn’t provide these leading zeros, but converting to bytes does. However, we can just add the 4 zeros to the start by ourselves to pad it correctly for a whole number of bytes. Doing this, we end up with

}2517ca3a_do0g_0n_N_11ams{ymedaca

which when reversed gives

academy{sma11_N_n0_g0od_a3ac7152}

…And that’s our flag! This one was just a bunch of math and a test of whether one knows what RSA is. The point, however, was to show that we need an NN so large that it’s not feasibly factorable, which happens at around 2048 bits or so. NEXT!

Mini RSA

N: 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e: 3
ciphertext (c): 5709720175026317841944505166332182779419760031115432215563425901875620052791608163398750297304277886495537367793006288112991932717211066611899319963701838062828209254458935983141207100322897221021063116398621664612418813778554568264340430225832578419510188716272485416847020031786240493474216219976022230869888952027678216917573096647103329007747318226924546155390806085120914969452892742334587155888067115809741247942520249413550823776542714197976954393983964547415885494497967317041948018762619837565110934913856322148114605081220228082382496419481186347789827332766319099768385339675485674098729928722491454024819209643705436617193628436345338680537047177323301194162660757103489761598885882853346119600569195748535459069143523542108427003139728980057083066664641150644821647353430627540846457261643465679095174704766947857556896761656169629741900371937539062751103533758611539748595561989792317060484717828643461989154370495487728398479845553917495920818130003766392290200672893336450122809476169268494312

If you were paying attention, the ciphertext cc when RSA done normally cannot be bigger than NN. However, here the ciphertext is. I don’t know what happened here but it doesn’t seem like the value for the ciphertext was done correctly. Let’s solve it anyways.

Recall the definition of ciphertext cc.

me≡cmod  Nm^e \equiv c \mod N

Notice how that if mem^e doesn’t exceed NN, then the public exponent ee is able to be used to get the original message since it doesn’t wrap around; just take the ee-th root of cc. Here, obviously it didn’t wrap around. Therefore we can just take the ee-th root here, which is also relatively quick since ee is so small. However, since we’re dealing with such a large number, it’d be easier to write our own function to cube root.

c = 5709720175026317841944505166332182779419760031115432215563425901875620052791608163398750297304277886495537367793006288112991932717211066611899319963701838062828209254458935983141207100322897221021063116398621664612418813778554568264340430225832578419510188716272485416847020031786240493474216219976022230869888952027678216917573096647103329007747318226924546155390806085120914969452892742334587155888067115809741247942520249413550823776542714197976954393983964547415885494497967317041948018762619837565110934913856322148114605081220228082382496419481186347789827332766319099768385339675485674098729928722491454024819209643705436617193628436345338680537047177323301194162660757103489761598885882853346119600569195748535459069143523542108427003139728980057083066664641150644821647353430627540846457261643465679095174704766947857556896761656169629741900371937539062751103533758611539748595561989792317060484717828643461989154370495487728398479845553917495920818130003766392290200672893336450122809476169268494312

def cube_root(x):
    lower, upper = 1, x
    mid = (lower+upper)//2
    while (mid**3!=x):
        if (mid**3 > x):
            upper=mid-1
        else:
            lower=mid+1

        mid = (lower+upper)//2

    return mid

print(cube_root(c))
# prints 1787330808968142828287809319332701517353332911736848279839502759158602467824780424488141955644417387373185756944952906538004355347478978500948630620749868180414755933760446136287315896825929319145984883756667607031853695069891380871892213007874933643544014803829752116973952588637032204981055419406087847822034466877741943243746534587658

Now, let’s see if ce\sqrt[e]{c} actually means anything. Converting to bits and then text, we get

€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€……‘•µåí•}Í ÁÔő}ˆÍ}±ɜÍÉ|Ì՘ÈٔŅô	

which doesn’t look right. The question said it was padded, and in binary we’re missing 2 bits to form a whole byte, so if we add one more 0 and remove the space padding, the text becomes

academy{e_sh0u1d_b3_lArg3r_35f26e1a}	

BANG!!! Well, this one was also straightforward. But let’s look at what it would have been like if the ciphertext was taken modulo NN like it was supposed to.

N: 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e: 3
ciphertext (c): 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345765474386082421144307534571699205194983368689411816226007643064954729215840635659583117133740500954205974875390057358188693046676903289930281540940412295980336146393416954525985992752798203045915510615113622948111778806320744724053802074960057287479336158454709711228187826016190033306377026203314361301932347821092319618619935053234187382079077148713322674442826911883069528540253556263918866573750163001632083631388027202763984747153601103043353896172647828985898572944046802007552386765619056890053190423953960201377144265891393217286098978682913818684143643056111293363915464884208356874230514968369175935792624184018193591389668970019543078419942628261961962015259372158970475685574715201244632387257077193912230917119974983481113661168899916641126944997341

It’s obvious to see that the ciphertext is not a cube root, so we can’t simply take the cube root and expect the flag. However, from the definition of modulus arithmetic, me=c+kNm^e = c + kN. Thus, we can simply bruteforce what mem^e could be by iterating k=1,k=2,k=… . This is a valid way for this one since the problem states that mem^e is just barely larger than N.

for k in range(max_k):
    candidate = c + k * N
    m = iroot(candidate, e)
    if m**e == candidate:
        break

« Previous: CyLab Security Academy - "New Caesar" writeup